package app import ( "os" "slices" "testing" "rickub.com/turbo-editors/turbo-core/tools" "rickub.com/turbo-editors/turbo-core/ui" ) // barMenuOf returns the menu with the given plain label, its items filled in. func barMenuOf(t *testing.T, a *App, plain string) *ui.Menu { t.Helper() for _, menu := range a.menu.Menus() { if ui.PlainLabel(menu.Label) != plain { continue } if menu.OnOpen != nil { menu.OnOpen() } return menu } t.Fatalf("there is no %q menu on the bar: %v", plain, barLabels(a)) return nil } // barLabels returns the bar's menus as plain names, for failure messages. func barLabels(a *App) []string { var names []string for _, menu := range a.menu.Menus() { names = append(names, ui.PlainLabel(menu.Label)) } return names } func TestAToolCanAskForAMenuOfItsOwn(t *testing.T) { a, _ := newToolsApp(t, ` [[tool]] name = "~E~cho" command = "echo TADA" menu = "Tools" `) menu := barMenuOf(t, a, "Tools") if len(menu.Items) != 1 || ui.PlainLabel(menu.Items[0].Label) != "Echo" { t.Errorf("the Tools menu holds %v", labels(menu.Items)) } } func TestAToolInItsOwnMenuIsNotAlsoInTheToolchainMenu(t *testing.T) { // A tool showing up twice would be worse than one showing up in the wrong // place: nobody would know which line does what. a, _ := newToolsApp(t, ` [[tool]] name = "Echo" command = "echo TADA" menu = "Tools" `) for _, item := range barMenuOf(t, a, a.toolsMenuName()).Items { if ui.PlainLabel(item.Label) == "Echo" { t.Errorf("Echo is in the %s menu as well as its own", a.toolsMenuName()) } } } func TestAToolThatNamesNoMenuStaysInTheToolchainMenu(t *testing.T) { a, _ := newToolsApp(t, ` [[tool]] name = "Test" command = "go test ./..." [[tool]] name = "Echo" command = "echo TADA" menu = "Tools" `) names := labels(barMenuOf(t, a, a.toolsMenuName()).Items) if len(names) == 0 || names[0] != "Test" { t.Errorf("the %s menu holds %v, wanted Test first", a.toolsMenuName(), names) } } func TestCreatedMenusFollowTheOrderOfTheFile(t *testing.T) { a, _ := newToolsApp(t, ` [[tool]] name = "Up" command = "docker compose up" menu = "Docker" [[tool]] name = "Echo" command = "echo x" menu = "Tools" `) got := barLabels(a) docker, echoMenu := positionOf(got, "Docker"), positionOf(got, "Tools") switch { case docker < 0 || echoMenu < 0: t.Fatalf("the bar is %v", got) case docker > echoMenu: t.Errorf("Docker comes after Tools on the bar: %v", got) } } func TestCreatedMenusSitBetweenTheToolchainMenuAndHelp(t *testing.T) { // Help is where Turbo C left it, and where a reader looks for it. a, _ := newToolsApp(t, "[[tool]]\nname = \"Echo\"\ncommand = \"echo x\"\nmenu = \"Tools\"\n") got := barLabels(a) toolchain := positionOf(got, a.toolsMenuName()) if created, help := positionOf(got, "Tools"), positionOf(got, "Help"); !(toolchain < created && created < help) { t.Errorf("the bar is %v", got) } } func TestNoCreatedMenuClashesWithAFixedOne(t *testing.T) { // The bar answers the first match, so a clash makes one of the two menus // unreachable from the keyboard — silently, and with every test passing. a, _ := newToolsApp(t, ` [[tool]] name = "a" command = "true" menu = "File" [[tool]] name = "b" command = "true" menu = "Search" [[tool]] name = "c" command = "true" menu = "Help" `) seen := map[rune]string{} for _, menu := range a.menu.Menus() { label, hot, _ := ui.SplitHotKey(menu.Label) if hot == 0 { continue // a name with no free letter left; F10 still reaches it } if other, clash := seen[hot]; clash { t.Errorf("%q and %q both answer to Alt-%c", other, label, hot) } seen[hot] = label } } func TestACreatedMenuTakesTheFirstFreeLetterOfItsName(t *testing.T) { a, _ := newToolsApp(t, "[[tool]]\nname = \"a\"\ncommand = \"true\"\nmenu = \"Format\"\n") // F is File's, o is Options', r is Run's — m is the first letter free. if label := barMenuOf(t, a, "Format").Label; label != "For~m~at" { t.Errorf("Format got the label %q, want For~m~at", label) } } func TestAMenuNameMayChooseItsOwnHotKey(t *testing.T) { // A free letter written into the name is kept. One that is already taken // is not — TestHotKeyLabelGivesUpAChosenLetterThatIsTaken covers that. a, _ := newToolsApp(t, "[[tool]]\nname = \"a\"\ncommand = \"true\"\nmenu = \"Doc~k~er\"\n") if label := barMenuOf(t, a, "Docker").Label; label != "Doc~k~er" { t.Errorf("the chosen hot key was not kept: %q", label) } } func TestHotKeyLabelFallsBackWhenEveryLetterIsTaken(t *testing.T) { taken := map[rune]bool{'a': true, 'b': true} // No hot key beats one that opens somebody else's menu. if got := hotKeyLabel("Ab", taken); got != "Ab" { t.Errorf("hotKeyLabel = %q, want the name unmarked", got) } } func TestHotKeyLabelGivesUpAChosenLetterThatIsTaken(t *testing.T) { taken := map[rune]bool{'f': true} if got := hotKeyLabel("~F~oo", taken); got != "F~o~o" { t.Errorf("hotKeyLabel = %q, want F~o~o", got) } } func TestHotKeyLabelReservesWhatItHandsOut(t *testing.T) { taken := map[rune]bool{} first := hotKeyLabel("Tools", taken) second := hotKeyLabel("Test", taken) if first == second { t.Fatalf("both menus got %q", first) } if second != "T~e~st" { t.Errorf("the second menu got %q, want T~e~st", second) } } func TestTheBarPicksUpAMenuAddedWhileRunning(t *testing.T) { // The set of menus comes from the file, so editing the file has to change // the bar; Menu.OnOpen only refills the items inside one. a, root := newToolsApp(t, "[[tool]]\nname = \"Test\"\ncommand = \"go test\"\n") if positionOf(barLabels(a), "Docker") >= 0 { t.Fatal("there is a Docker menu before the file asks for one") } writeTools(t, root, "[[tool]]\nname = \"Up\"\ncommand = \"docker compose up\"\nmenu = \"Docker\"\n") a.tick() if positionOf(barLabels(a), "Docker") < 0 { t.Errorf("the bar is still %v", barLabels(a)) } } func TestTheBarDropsAMenuTheFileNoLongerAsksFor(t *testing.T) { a, root := newToolsApp(t, "[[tool]]\nname = \"Up\"\ncommand = \"docker compose up\"\nmenu = \"Docker\"\n") if positionOf(barLabels(a), "Docker") < 0 { t.Fatalf("the bar starts as %v", barLabels(a)) } writeTools(t, root, "[[tool]]\nname = \"Test\"\ncommand = \"go test ./...\"\n") a.tick() if positionOf(barLabels(a), "Docker") >= 0 { t.Errorf("the bar is still %v", barLabels(a)) } } func TestAnUnchangedToolsFileLeavesTheBarAlone(t *testing.T) { // Rebuilding on every turn of the loop would close a menu under whoever // was reading it, and throw away the OnOpen closures each turn. a, _ := newToolsApp(t, "[[tool]]\nname = \"Echo\"\ncommand = \"echo x\"\nmenu = \"Tools\"\n") before := a.menu.Menus() a.tick() if after := a.menu.Menus(); &before[0] != &after[0] { t.Error("the bar was rebuilt although the tools file had not changed") } } func TestAnUnreadableToolsFileCreatesNoMenus(t *testing.T) { // The Go menu already says the file cannot be read; saying it again once // per menu the file was about to create would be noise. a, _ := newToolsApp(t, "[[tool]\nname = broken\n") // Named rather than counted: a count says nothing about *which* menu // appeared, and this list is also the one place the whole fixed bar is // written down, which a tutorial's "press → five times" depends on. want := []string{"File", "Edit", "Search", "Run", "Code", "Options", "Window", "Snippets", "Agent", "Test", "Help"} if got := barLabels(a); !slices.Equal(got, want) { t.Errorf("the bar is %v, want %v", got, want) } } // writeTools replaces a project's tools file. func writeTools(t *testing.T, root, contents string) { t.Helper() if err := os.WriteFile(tools.Path(testProfile(), root), []byte(contents), 0o644); err != nil { t.Fatalf("writing the tools file: %v", err) } } // positionOf returns where a name sits in a list, or -1 when it is absent. func positionOf(names []string, want string) int { for i, name := range names { if name == want { return i } } return -1 }