package ui import ( "strings" "testing" "github.com/gdamore/tcell/v2" ) // submenuBar returns a bar whose second menu holds one plain item and one // branch, and records what was run. func submenuBar() (*MenuBar, *[]string) { var ran []string record := func(name string) func() { return func() { ran = append(ran, name) } } bar := NewMenuBar( &Menu{Label: "~F~ile", Items: []*MenuItem{ {Label: "~N~ew", Action: record("new")}, }}, &Menu{Label: "~S~nippets", Items: []*MenuItem{ {Label: "~G~o", Items: []*MenuItem{ {Label: "~f~unc", Action: record("go/func")}, {Label: "if ~e~rr", Action: record("go/iferr")}, }}, {Label: "~S~hell", Items: []*MenuItem{ {Label: "~l~oop", Action: record("shell/loop")}, }}, {Separator: true}, {Label: "~C~reate file", Action: record("create")}, }}, ) bar.SetBounds(Rect{W: 80, H: 1}) return bar, &ran } // key sends a key to the bar. func key(bar *MenuBar, k tcell.Key) bool { return bar.HandleKey(tcell.NewEventKey(k, 0, tcell.ModNone)) } // typeRune sends a printable key to the bar. func typeRune(bar *MenuBar, r rune) bool { return bar.HandleKey(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)) } func TestAnItemWithNoActionButASubmenuCanStillBeChosen(t *testing.T) { // Without this an item that only opens a branch counts as disabled, and // the whole submenu is unreachable. branch := &MenuItem{Label: "Go", Items: []*MenuItem{{Label: "f", Action: func() {}}}} if !branch.enabled() { t.Error("an item with a submenu and no action reports itself disabled") } } func TestRightOpensTheHighlightedSubmenu(t *testing.T) { bar, _ := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) if !bar.submenuOpen() { t.Fatal("right did not open the submenu") } if bar.openItem != 0 { t.Errorf("openItem = %d, want the first branch", bar.openItem) } } func TestRightMovesToTheNextMenuWhenThereIsNoSubmenu(t *testing.T) { bar, _ := submenuBar() bar.OpenMenu(0) // File, whose only item is plain key(bar, tcell.KeyRight) if bar.submenuOpen() { t.Error("right opened a submenu on an item that has none") } if bar.openMenu != 1 { t.Errorf("openMenu = %d, want the next menu", bar.openMenu) } } func TestEnterOnABranchOpensItRatherThanRunningAnything(t *testing.T) { bar, ran := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyEnter) if !bar.submenuOpen() { t.Error("enter on a branch did not open it") } if len(*ran) != 0 { t.Errorf("enter on a branch ran %v", *ran) } } func TestTheArrowsWalkTheSubmenu(t *testing.T) { bar, _ := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) key(bar, tcell.KeyDown) if bar.subHighlight != 1 { t.Errorf("subHighlight = %d, want 1", bar.subHighlight) } key(bar, tcell.KeyUp) if bar.subHighlight != 0 { t.Errorf("subHighlight = %d, want 0", bar.subHighlight) } } func TestEnterInASubmenuRunsTheItemAndClosesEverything(t *testing.T) { bar, ran := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) key(bar, tcell.KeyDown) key(bar, tcell.KeyEnter) if len(*ran) != 1 || (*ran)[0] != "go/iferr" { t.Errorf("ran %v, want the second submenu item", *ran) } if bar.Open() || bar.submenuOpen() { t.Error("running a submenu item left a menu showing") } } func TestLeftStepsBackOutOfASubmenu(t *testing.T) { bar, _ := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) key(bar, tcell.KeyLeft) if bar.submenuOpen() { t.Error("left did not close the submenu") } if !bar.Open() { t.Error("left closed the whole menu instead of stepping out") } } func TestEscapeFromASubmenuClosesTheWholeMenu(t *testing.T) { // Cancel should mean cancel wherever you are, which is what makes it // different from left. bar, _ := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) key(bar, tcell.KeyEscape) if bar.Open() || bar.submenuOpen() { t.Error("escape left a menu showing") } } func TestAHotKeyInsideASubmenuRunsItsItem(t *testing.T) { bar, ran := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) typeRune(bar, 'e') // "if ~e~rr" if len(*ran) != 1 || (*ran)[0] != "go/iferr" { t.Errorf("ran %v, want the item whose hot key was typed", *ran) } } func TestAHotKeyOnABranchOpensItRatherThanRunningIt(t *testing.T) { bar, ran := submenuBar() bar.OpenMenu(1) typeRune(bar, 's') // "~S~hell" if !bar.submenuOpen() || bar.openItem != 1 { t.Errorf("openItem = %d, submenuOpen = %v; want the Shell branch open", bar.openItem, bar.submenuOpen()) } if len(*ran) != 0 { t.Errorf("a hot key on a branch ran %v", *ran) } } func TestASubmenuSwallowsStrayTyping(t *testing.T) { bar, ran := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) if !typeRune(bar, 'z') { t.Error("a submenu let a stray key through to whatever is behind it") } if len(*ran) != 0 { t.Errorf("a stray key ran %v", *ran) } } func TestOpeningAnotherMenuClosesTheSubmenu(t *testing.T) { bar, _ := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) bar.OpenMenu(0) if bar.submenuOpen() { t.Error("opening another menu left the submenu showing") } } func TestASubmenuIsDrawnBesideItsParentWithAMarkerOnTheBranch(t *testing.T) { screen, root := newTestScreen(t, 80, 12) bar, _ := submenuBar() bar.SetBounds(Rect{W: 80, H: 1}) bar.OpenMenu(1) key(bar, tcell.KeyRight) bar.Draw(root, testTheme(t)) screen.Show() lines := screenLines(t, screen) shown := strings.Join(lines, "\n") if !strings.Contains(shown, submenuMarker) { t.Errorf("no branch marker is drawn:\n%s", shown) } for _, want := range []string{"Go", "Shell", "func", "if err"} { if !strings.Contains(shown, want) { t.Errorf("%q is not on the screen:\n%s", want, shown) } } // The two panels must not sit on top of each other. parent, sub := bar.dropdownBounds(), bar.submenuBounds() if parent.Intersect(sub).W > 0 && parent.Intersect(sub).H > 0 { t.Errorf("the panels overlap: parent %+v, submenu %+v", parent, sub) } } func TestASubmenuFlipsLeftWhenThereIsNoRoomOnTheRight(t *testing.T) { // A menu drawn past the edge of the screen is a menu nobody can read. bar := NewMenuBar( &Menu{Label: "A", Items: nil}, &Menu{Label: "B", Items: nil}, &Menu{Label: "~S~nippets", Items: []*MenuItem{ {Label: "A branch with a long name", Items: []*MenuItem{ {Label: "an item with a very long label indeed", Action: func() {}}, }}, }}, ) bar.SetBounds(Rect{W: 40, H: 1}) bar.OpenMenu(2) key(bar, tcell.KeyRight) sub := bar.submenuBounds() if sub.Right() > bar.Bounds().Right() { t.Errorf("the submenu at %+v runs past the screen %+v", sub, bar.Bounds()) } if sub.X < bar.Bounds().X { t.Errorf("the submenu at %+v starts before the screen", sub) } } func TestMovingThePointerOntoABranchOpensItAndOffItClosesIt(t *testing.T) { bar, _ := submenuBar() bar.OpenMenu(1) bounds := bar.dropdownBounds() // Hover the first branch. bar.HandleMouse(tcell.NewEventMouse(bounds.X+2, bounds.Y+1, tcell.ButtonNone, tcell.ModNone)) if !bar.submenuOpen() { t.Fatal("hovering a branch did not open its submenu") } // Hover the plain item further down. bar.HandleMouse(tcell.NewEventMouse(bounds.X+2, bounds.Y+4, tcell.ButtonNone, tcell.ModNone)) if bar.submenuOpen() { t.Error("moving off a branch left its submenu showing") } } func TestClickingASubmenuItemRunsIt(t *testing.T) { bar, ran := submenuBar() bar.OpenMenu(1) key(bar, tcell.KeyRight) sub := bar.submenuBounds() bar.HandleMouse(tcell.NewEventMouse(sub.X+2, sub.Y+1, tcell.Button1, tcell.ModNone)) if len(*ran) != 1 || (*ran)[0] != "go/func" { t.Errorf("ran %v, want the clicked submenu item", *ran) } } func TestOnOpenRunsBeforeTheMenuDropsDown(t *testing.T) { // A menu built from a file cannot be decided at start-up; this is the hook // that fills it in at the only moment it is about to be seen. filled := 0 menu := &Menu{Label: "~S~nippets"} menu.OnOpen = func() { filled++ menu.Items = []*MenuItem{{Label: "from the file", Action: func() {}}} } bar := NewMenuBar(menu) bar.SetBounds(Rect{W: 80, H: 1}) bar.OpenMenu(0) if filled != 1 { t.Errorf("OnOpen ran %d times, want once", filled) } if len(menu.Items) != 1 { t.Fatalf("the menu holds %d items after OnOpen", len(menu.Items)) } if bar.highlight != 0 { t.Errorf("highlight = %d; the highlight must be computed after OnOpen filled the menu", bar.highlight) } } func TestAnEmptyMenuOpensWithoutPanicking(t *testing.T) { bar := NewMenuBar(&Menu{Label: "~S~nippets"}) bar.SetBounds(Rect{W: 80, H: 1}) bar.OpenMenu(0) key(bar, tcell.KeyDown) key(bar, tcell.KeyRight) key(bar, tcell.KeyEnter) }