package app import ( "os" "path/filepath" "strings" "testing" "github.com/gdamore/tcell/v2" "codeberg.org/turbo-editors/turbo-core/snippets" "codeberg.org/turbo-editors/turbo-core/ui" ) // newSnippetApp returns an editor whose working directory is a project holding // the given snippets file, and whose user-level snippets are empty. func newSnippetApp(t *testing.T, snippetsTOML string) (*App, tcell.SimulationScreen, string) { t.Helper() root := t.TempDir() t.Chdir(root) t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir()) // never read whoever runs the tests if snippetsTOML != "" { path := snippets.ProjectPath(testProfile(), root) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("creating the snippets directory: %v", err) } if err := os.WriteFile(path, []byte(snippetsTOML), 0o644); err != nil { t.Fatalf("writing the snippets file: %v", err) } } a, screen := newTestApp(t) return a, screen, root } // snippetsMenuOf returns the Snippets menu with its items filled in, which is // what OnOpen does just before it drops down. func snippetsMenuOf(t *testing.T, a *App) *ui.Menu { t.Helper() for _, menu := range a.menu.Menus() { if ui.PlainLabel(menu.Label) != "Snippets" { continue } if menu.OnOpen == nil { t.Fatal("the Snippets menu has no OnOpen, so it can never be filled") } menu.OnOpen() return menu } t.Fatal("there is no Snippets menu on the bar") return nil } // labels returns the plain labels of a list of menu items. // sameLabels reports whether two lists of menu labels are equal, in order. func sameLabels(got, want []string) bool { if len(got) != len(want) { return false } for i := range want { if got[i] != want[i] { return false } } return true } func labels(items []*ui.MenuItem) []string { out := make([]string, 0, len(items)) for _, item := range items { if item.Separator { out = append(out, "---") continue } out = append(out, ui.PlainLabel(item.Label)) } return out } // findItem returns the item with a plain label, or nil. func findItem(items []*ui.MenuItem, label string) *ui.MenuItem { for _, item := range items { if ui.PlainLabel(item.Label) == label { return item } } return nil } const twoGroups = ` [[snippet]] name = "if broken" group = "Shell" languages = ["bash"] body = """ if [ -n "$broken" ]; then \treturn 1 fi""" [[snippet]] name = "TODO" body = "TODO: " ` func TestNoTwoMenusShareAHotKey(t *testing.T) { // The bar answers the first match it finds, so a duplicate silently makes // one menu unreachable from the keyboard. Snippets and Search both wanted // S, and Snippets was the one nobody could open. a, _ := newTestApp(t) seen := map[rune]string{} for _, menu := range a.menu.Menus() { label, hot, _ := ui.SplitHotKey(menu.Label) if hot == 0 { t.Errorf("the %q menu has no hot key", label) continue } if other, clash := seen[hot]; clash { t.Errorf("%q and %q both answer to Alt-%c", other, label, hot) } seen[hot] = label } } func TestThereIsAlwaysASnippetsMenu(t *testing.T) { // Without one, the item that creates the file is unreachable and the // feature cannot be started. a, _, _ := newSnippetApp(t, "") menu := snippetsMenuOf(t, a) want := []string{"Create snippets file", "Open snippets file"} if got := labels(menu.Items); !sameLabels(got, want) { t.Errorf("an empty project's Snippets menu holds %v, want %v", got, want) } } func TestTheMenuIsBuiltFromTheFile(t *testing.T) { a, _, root := newSnippetApp(t, twoGroups) writeTestFile(t, filepath.Join(root, "build.sh"), "#!/bin/sh\n") a.Open(filepath.Join(root, "build.sh")) menu := snippetsMenuOf(t, a) if got := labels(menu.Items); len(got) != 5 || got[0] != "Shell" || got[1] != "General" { t.Fatalf("the menu holds %v, want the two groups then the create and open items", got) } group := findItem(menu.Items, "Shell") if group == nil || len(group.Items) != 1 { t.Fatalf("the Shell group holds %v", labels(group.Items)) } if got := ui.PlainLabel(group.Items[0].Label); got != "if broken" { t.Errorf("the Shell submenu holds %q", got) } } func TestTheMenuIsFilteredByTheFrontWindowsLanguage(t *testing.T) { a, _, root := newSnippetApp(t, twoGroups) writeTestFile(t, filepath.Join(root, "notes.md"), "# Notes\n") a.Open(filepath.Join(root, "notes.md")) menu := snippetsMenuOf(t, a) if findItem(menu.Items, "Shell") != nil { t.Errorf("a shell snippet is offered in a Markdown file: %v", labels(menu.Items)) } if findItem(menu.Items, "General") == nil { t.Errorf("the snippet that names no language is not offered: %v", labels(menu.Items)) } } func TestWithNoFileOpenEveryUnrestrictedSnippetIsOffered(t *testing.T) { a, _, _ := newSnippetApp(t, twoGroups) menu := snippetsMenuOf(t, a) if findItem(menu.Items, "General") == nil { t.Errorf("nothing is offered with no file open: %v", labels(menu.Items)) } if findItem(menu.Items, "Shell") != nil { t.Errorf("a shell-only snippet is offered with no file open: %v", labels(menu.Items)) } } func TestTheMenuFollowsTheWindowThatIsInFront(t *testing.T) { // OnOpen is the whole point: the menu cannot be decided at start-up. a, _, root := newSnippetApp(t, twoGroups) writeTestFile(t, filepath.Join(root, "build.sh"), "#!/bin/sh\n") writeTestFile(t, filepath.Join(root, "notes.md"), "# Notes\n") a.Open(filepath.Join(root, "build.sh")) a.Open(filepath.Join(root, "notes.md")) if findItem(snippetsMenuOf(t, a).Items, "Shell") != nil { t.Error("the Shell group is offered while a Markdown file is in front") } a.NextWindow() // back to build.sh if findItem(snippetsMenuOf(t, a).Items, "Shell") == nil { t.Error("the Shell group is not offered after moving to the shell file") } } func TestChoosingASnippetInsertsItAtTheCursor(t *testing.T) { a, _, root := newSnippetApp(t, twoGroups) writeTestFile(t, filepath.Join(root, "build.sh"), "f() {\n\t\n}\n") a.Open(filepath.Join(root, "build.sh")) press(a, tcell.KeyDown, 0, tcell.ModNone) // onto the indented blank line press(a, tcell.KeyEnd, 0, tcell.ModNone) group := findItem(snippetsMenuOf(t, a).Items, "Shell") group.Items[0].Action() want := "f() {\n\tif [ -n \"$broken\" ]; then\n\t\treturn 1\n\tfi\n}\n" if got := activeBuffer(t, a).Text(); got != want { t.Errorf("the buffer holds\n%q\nwant\n%q", got, want) } } func TestASnippetItemIsDisabledWithNoFileOpen(t *testing.T) { a, _, _ := newSnippetApp(t, twoGroups) group := findItem(snippetsMenuOf(t, a).Items, "General") if group == nil || len(group.Items) == 0 { t.Fatal("the General group is missing") } if group.Items[0].Enabled == nil || group.Items[0].Enabled() { t.Error("a snippet can be chosen with no file to insert it into") } } func TestInsertingWithNoFileOpenDoesNothing(t *testing.T) { a, _, _ := newSnippetApp(t, twoGroups) a.InsertSnippet("anything") if a.Desktop().Count() != 0 { t.Error("inserting a snippet with no window opened one") } if a.Modals() != 0 { t.Error("inserting a snippet with no window opened a dialog") } } func TestAnUnreadableSnippetsFileIsSaidSoInTheMenu(t *testing.T) { // A typo should be visible where the snippets were expected, not silent. a, _, _ := newSnippetApp(t, "[[snippet]\nname = ") items := snippetsMenuOf(t, a).Items if got := labels(items); len(got) == 0 || !strings.Contains(got[0], "Cannot read") { t.Errorf("the menu holds %v, want a line saying the file cannot be read", got) } if items[0].Enabled == nil || items[0].Enabled() { t.Error("the error line can be chosen") } if findItem(items, "Create snippets file") == nil { t.Error("the create item is missing, so there is no way forward") } } func TestCreateSnippetsWritesAndOpensTheFile(t *testing.T) { a, _, root := newSnippetApp(t, "") a.CreateSnippets() if !snippets.Exists(testProfile(), root) { t.Fatal("no snippets file was written") } if a.Desktop().Count() != 1 { t.Fatalf("Count() = %d, want the file open", a.Desktop().Count()) } if got := a.Desktop().Active().Title(); got != snippets.FileName { t.Errorf("the window shows %q, want %q", got, snippets.FileName) } } func TestTheCreatedFileFillsTheMenu(t *testing.T) { // The starter file has to be usable, not merely parseable. Which groups // are in it belongs to the editor's own template and its own test; what is // checked here is that whatever it holds reaches the menu. a, _, root := newSnippetApp(t, "") a.CreateSnippets() writeTestFile(t, filepath.Join(root, "build.sh"), "#!/bin/sh\n") a.Open(filepath.Join(root, "build.sh")) items := snippetsMenuOf(t, a).Items group := findItem(items, "General") if group == nil || len(group.Items) == 0 { t.Errorf("the created file gave no snippets at all: %v", labels(items)) } } func TestCreateSnippetsTwiceOpensRatherThanOverwrites(t *testing.T) { a, _, root := newSnippetApp(t, "[[snippet]]\nname = \"mine\"\nbody = \"hand written\"\n") a.CreateSnippets() contents := readTestFile(t, snippets.ProjectPath(testProfile(), root)) if !strings.Contains(contents, "hand written") { t.Errorf("the existing file was overwritten:\n%s", contents) } if a.Desktop().Count() != 1 { t.Errorf("Count() = %d, want the existing file opened", a.Desktop().Count()) } } func TestTheSnippetsFileIsColouredAsTOML(t *testing.T) { a, screen, _ := newSnippetApp(t, "") a.CreateSnippets() lines := render(t, a, screen) row, col := findRune(t, lines, "# turbo-test snippets", '#') cells, width, _ := screen.GetContents() got, _, _ := cells[row*width+col].Style.Decompose() want, _, _ := a.Theme().Style("syntax.comment").Decompose() if got != want { t.Errorf("the comment is drawn in %v, want the comment colour %v", got, want) } }