The menu is exercised alone, through menuHost, and the model is checked to implement each in-package host.
- commit
- b4931c8dae918e1d4991a2f8ebc12e8c643ddc67
- parent
- f28e91aa083c0bd9df36cfcb056671bee3b9227a
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 7 | +++ |
| A | internal/ | 82 | ++++++++++++++++++++++++++++++ |
@@ -27,6 +27,13 @@ var (
2727 _ themepicker.Host = host{}2828 )2929 30+// The in-package hosts the model implements itself.31+var (32+ _ menuHost = (*Model)(nil)33+ _ pivotHost = (*Model)(nil)34+ _ macrosHost = (*Model)(nil)35+)36+3037 // Components that stay in package ui are handed interfaces of their own3138 // (menuHost and the like), which the model implements with unexported3239 // methods, these among them.@@ -0,0 +1,82 @@
1+package ui2+3+import (4+ "strings"5+ "testing"6+7+ tea "charm.land/bubbletea/v2"8+ "github.com/charmbracelet/x/ansi"9+10+ "github.com/FelineStateMachine/012/internal/ui/overlay"11+ "github.com/FelineStateMachine/012/internal/ui/theme"12+)13+14+// fakeMenuHost is a menu's host without a model: every command is15+// available but "off", and what the menu asks for is recorded.16+type fakeMenuHost struct {17+ th theme.Theme18+ ran []string19+ closed int20+ bar int21+ right [2]int22+}23+24+func (f *fakeMenuHost) styles() *theme.Theme { return &f.th }25+func (f *fakeMenuHost) size() (width, height int) { return 80, 24 }26+func (f *fakeMenuHost) available(id string) bool { return id != "off" }27+func (f *fakeMenuHost) shortcut(id string) string { return map[string]string{"a": "Ctrl+A"}[id] }28+func (f *fakeMenuHost) isChecked(id string) bool { return id == "b" }29+func (f *fakeMenuHost) showBarMenu(i int) { f.bar = i }30+func (f *fakeMenuHost) rightClick(x, y int) { f.right = [2]int{x, y} }31+func (f *fakeMenuHost) closeOverlay() { f.closed++ }32+33+func (f *fakeMenuHost) runFromOverlay(id string) tea.Cmd {34+ f.ran = append(f.ran, id)35+ return nil36+}37+38+func fakeMenu(h menuHost) *menuOverlay {39+ items := []menuItem{40+ {title: "Alpha", cmd: "a"}, {title: "Beta", cmd: "b"}, sep,41+ {title: "Off", cmd: "off"},42+ {title: "More", items: []menuItem{{title: "Gamma", cmd: "c"}}},43+ }44+ return &menuOverlay{m: h, bar: -1, x: 10, y: 5, levels: []*menuLevel{newLevel(h, items, true)}}45+}46+47+func TestMenuAlone(t *testing.T) {48+ h := &fakeMenuHost{th: theme.New(true), bar: -1}49+ o := fakeMenu(h)50+ b := o.Layout()[0]51+ out := ansi.Strip(strings.Join(b.Lines, "\n"))52+ for _, want := range []string{"Alpha", "Ctrl+A", "Beta", "✓", "Off", "›"} {53+ if !strings.Contains(out, want) {54+ t.Errorf("menu lacks %q:\n%s", want, out)55+ }56+ }57+ if b.X != 10 || b.Y != 5 {58+ t.Errorf("menu at %d,%d", b.X, b.Y)59+ }60+ // Down skips the separator and the unavailable item.61+ o.Key(tea.KeyPressMsg{Code: tea.KeyDown})62+ o.Key(tea.KeyPressMsg{Code: tea.KeyDown})63+ if it := o.top().items[o.top().sel]; it.title != "More" {64+ t.Fatalf("highlighted %q", it.title)65+ }66+ o.Key(tea.KeyPressMsg{Code: tea.KeyRight})67+ o.Key(tea.KeyPressMsg{Code: tea.KeyEnter})68+ if len(o.levels) != 2 || strings.Join(h.ran, ",") != "c" {69+ t.Errorf("levels %d, ran %v", len(o.levels), h.ran)70+ }71+ // A letter runs the only item starting with it.72+ o.Key(tea.KeyPressMsg{Code: tea.KeyEscape})73+ o.Key(tea.KeyPressMsg{Code: 'b', Text: "b"})74+ if strings.Join(h.ran, ",") != "c,b" {75+ t.Errorf("ran %v", h.ran)76+ }77+ // Right-clicking outside closes the menu and opens one there.78+ o.Mouse(overlay.MouseEvent{Kind: overlay.MousePress, Button: tea.MouseRight, X: 50, Y: 20})79+ if h.closed != 1 || h.right != [2]int{50, 20} {80+ t.Errorf("closed %d, right-click at %v", h.closed, h.right)81+ }82+}