package ui import ( "strings" "testing" "github.com/gdamore/tcell/v2" ) // testMenuBar builds a bar of two menus and returns it with a log the actions // append to, so a test can see which item ran. func testMenuBar() (*MenuBar, *[]string) { var log []string record := func(name string) func() { return func() { log = append(log, name) } } file := &Menu{Label: "~F~ile", Items: []*MenuItem{ {Label: "~N~ew", Shortcut: "F4", Action: record("new")}, {Label: "~O~pen…", Shortcut: "F3", Action: record("open")}, {Separator: true}, {Label: "~S~ave", Shortcut: "F2", Action: record("save"), Enabled: func() bool { return false }}, {Label: "E~x~it", Shortcut: "Alt-X", Action: record("exit")}, }} edit := &Menu{Label: "~E~dit", Items: []*MenuItem{ {Label: "~U~ndo", Action: record("undo")}, }} return NewMenuBar(file, edit), &log } func TestAMenuBarStartsClosed(t *testing.T) { bar, _ := testMenuBar() if bar.Open() { t.Error("Open() = true on a new menu bar") } if len(bar.Menus()) != 2 { t.Errorf("Menus() returned %d menus, want 2", len(bar.Menus())) } } func TestF10OpensTheFirstMenu(t *testing.T) { bar, _ := testMenuBar() if !bar.HandleKey(keyEvent(tcell.KeyF10, 0, tcell.ModNone)) { t.Fatal("F10 was not handled") } if !bar.Open() { t.Error("F10 did not open a menu") } } func TestAltLetterOpensTheMatchingMenu(t *testing.T) { bar, _ := testMenuBar() if !bar.HandleKey(keyEvent(tcell.KeyRune, 'e', tcell.ModAlt)) { t.Fatal("Alt-E was not handled") } if bar.openMenu != 1 { t.Errorf("Alt-E opened menu %d, want the Edit menu", bar.openMenu) } } func TestAltLetterThatMatchesNothingIsLeftAlone(t *testing.T) { bar, _ := testMenuBar() if bar.HandleKey(keyEvent(tcell.KeyRune, 'z', tcell.ModAlt)) { t.Error("Alt-Z was claimed although no menu answers to it") } if bar.HandleKey(runeEvent('f')) { t.Error("a plain letter opened a menu; only Alt-letter should") } } func TestOpeningAMenuHighlightsItsFirstUsableItem(t *testing.T) { bar, _ := testMenuBar() bar.OpenMenu(0) if bar.highlight != 0 { t.Errorf("highlight = %d, want the first item", bar.highlight) } } func TestArrowsWalkPastSeparatorsAndDisabledItems(t *testing.T) { bar, _ := testMenuBar() bar.OpenMenu(0) // New, Open…, ─, Save (disabled), Exit bar.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone)) if bar.highlight != 1 { t.Fatalf("highlight = %d, want 1", bar.highlight) } bar.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone)) if bar.highlight != 4 { t.Errorf("highlight = %d, want 4 — the separator and the disabled item are skipped", bar.highlight) } bar.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone)) if bar.highlight != 0 { t.Errorf("highlight = %d, want it to wrap back to the first item", bar.highlight) } bar.HandleKey(keyEvent(tcell.KeyUp, 0, tcell.ModNone)) if bar.highlight != 4 { t.Errorf("highlight = %d, want it to wrap backwards to the last usable item", bar.highlight) } } func TestLeftAndRightMoveBetweenMenus(t *testing.T) { bar, _ := testMenuBar() bar.OpenMenu(0) bar.HandleKey(keyEvent(tcell.KeyRight, 0, tcell.ModNone)) if bar.openMenu != 1 { t.Errorf("openMenu = %d after Right, want 1", bar.openMenu) } bar.HandleKey(keyEvent(tcell.KeyRight, 0, tcell.ModNone)) if bar.openMenu != 0 { t.Errorf("openMenu = %d, want it to wrap round to the first menu", bar.openMenu) } bar.HandleKey(keyEvent(tcell.KeyLeft, 0, tcell.ModNone)) if bar.openMenu != 1 { t.Errorf("openMenu = %d after Left from the first menu, want the last one", bar.openMenu) } } func TestEnterRunsTheHighlightedItemAndClosesTheMenu(t *testing.T) { bar, log := testMenuBar() bar.OpenMenu(0) bar.HandleKey(keyEvent(tcell.KeyEnter, 0, tcell.ModNone)) if len(*log) != 1 || (*log)[0] != "new" { t.Errorf("the action log is %v, want [new]", *log) } if bar.Open() { t.Error("the menu stayed open after an item ran") } } func TestTypingAnItemsHotKeyRunsIt(t *testing.T) { bar, log := testMenuBar() bar.OpenMenu(0) bar.HandleKey(runeEvent('x')) if len(*log) != 1 || (*log)[0] != "exit" { t.Errorf("the action log is %v, want [exit]", *log) } } func TestADisabledItemCannotBeRun(t *testing.T) { bar, log := testMenuBar() bar.OpenMenu(0) bar.HandleKey(runeEvent('s')) // Save is disabled if len(*log) != 0 { t.Errorf("the action log is %v, want it empty", *log) } if !bar.Open() { t.Error("the menu closed although nothing ran") } } func TestAnOpenMenuSwallowsStrayTyping(t *testing.T) { bar, _ := testMenuBar() bar.OpenMenu(0) if !bar.HandleKey(runeEvent('q')) { t.Error("a letter that matches nothing escaped the open menu and would have reached the editor") } } func TestEscapeClosesTheMenu(t *testing.T) { bar, _ := testMenuBar() bar.OpenMenu(0) bar.HandleKey(keyEvent(tcell.KeyEscape, 0, tcell.ModNone)) if bar.Open() { t.Error("Escape did not close the menu") } } func TestAKeyTheOpenMenuHasNoUseForIsPassedOn(t *testing.T) { bar, _ := testMenuBar() bar.OpenMenu(0) if bar.HandleKey(keyEvent(tcell.KeyF5, 0, tcell.ModNone)) { t.Error("the open menu claimed a function key it does nothing with") } } func TestOpenMenuIgnoresAnIndexOutOfRange(t *testing.T) { bar, _ := testMenuBar() bar.OpenMenu(99) if bar.Open() { t.Error("OpenMenu(99) opened something") } } func TestTheMenuBarDrawsItsTitlesAndTheOpenDropdown(t *testing.T) { screen, root := newTestScreen(t, 40, 12) bar, _ := testMenuBar() bar.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 1}) bar.OpenMenu(0) bar.Draw(root, testTheme(t)) screen.Show() lines := screenLines(t, screen) if !strings.Contains(lines[0], "File") || !strings.Contains(lines[0], "Edit") { t.Errorf("the bar is %q, want both menu titles", lines[0]) } if !strings.Contains(lines[2], "New") { t.Errorf("row 2 is %q, want the first item of the open menu", lines[2]) } if !strings.Contains(lines[2], "F4") { t.Errorf("row 2 is %q, want the item's shortcut shown", lines[2]) } if !strings.Contains(lines[4], "─") { t.Errorf("row 4 is %q, want the separator drawn as a rule", lines[4]) } } func TestClickingATitleOpensAndClosesItsMenu(t *testing.T) { bar, _ := testMenuBar() bar.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 1}) bar.HandleMouse(clickEvent(2, 0)) // on "File" if bar.openMenu != 0 { t.Fatalf("openMenu = %d, want the File menu open", bar.openMenu) } bar.HandleMouse(clickEvent(2, 0)) // the same title again if bar.Open() { t.Error("clicking the open menu's own title did not close it") } } func TestClickingAnItemRunsIt(t *testing.T) { bar, log := testMenuBar() bar.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 1}) bar.OpenMenu(0) bounds := bar.dropdownBounds() bar.HandleMouse(clickEvent(bounds.X+2, bounds.Y+2)) // the second item, "Open…" if len(*log) != 1 || (*log)[0] != "open" { t.Errorf("the action log is %v, want [open]", *log) } } func TestClickingAwayFromAnOpenMenuClosesIt(t *testing.T) { bar, log := testMenuBar() bar.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 1}) bar.OpenMenu(0) bar.HandleMouse(clickEvent(38, 9)) if bar.Open() { t.Error("a click elsewhere did not close the menu") } if len(*log) != 0 { t.Errorf("the action log is %v, want it empty", *log) } } func TestMovingOverAnItemHighlightsIt(t *testing.T) { bar, log := testMenuBar() bar.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 1}) bar.OpenMenu(0) bounds := bar.dropdownBounds() bar.HandleMouse(tcell.NewEventMouse(bounds.X+2, bounds.Y+2, tcell.ButtonNone, tcell.ModNone)) if bar.highlight != 1 { t.Errorf("highlight = %d, want the item under the pointer", bar.highlight) } if len(*log) != 0 { t.Error("merely moving over an item ran it") } } func TestAClickThatMissesAClosedBarIsNotHandled(t *testing.T) { bar, _ := testMenuBar() bar.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 1}) if bar.HandleMouse(clickEvent(10, 6)) { t.Error("a closed menu bar claimed a click well below it") } } func TestSetMenusReplacesWhatTheBarShows(t *testing.T) { bar := NewMenuBar(&Menu{Label: "~F~ile", Items: []*MenuItem{{Label: "Open"}}}) bar.SetMenus([]*Menu{{Label: "~T~ools", Items: []*MenuItem{{Label: "Echo"}}}}) if got := bar.Menus(); len(got) != 1 || PlainLabel(got[0].Label) != "Tools" { t.Fatalf("Menus() = %v, want just Tools", got) } } func TestSetMenusClosesWhatWasOpen(t *testing.T) { // The open index points into the old slice: keeping it would drop down // whichever menu happened to take that position, or index out of range. bar := NewMenuBar( &Menu{Label: "~F~ile", Items: []*MenuItem{{Label: "Open"}}}, &Menu{Label: "~E~dit", Items: []*MenuItem{{Label: "Copy"}}}, ) bar.OpenMenu(1) bar.SetMenus([]*Menu{{Label: "~T~ools", Items: []*MenuItem{{Label: "Echo"}}}}) if bar.Open() { t.Error("the bar is still open after its menus were replaced") } }