1package ui
2
3import (
4 "testing"
5
6 "github.com/bnema/dumber/internal/domain/entity"
7 "github.com/bnema/dumber/internal/ui/input"
8 "github.com/bnema/dumber/internal/ui/layout"
9 "github.com/bnema/dumber/internal/ui/layout/mocks"
10 "github.com/stretchr/testify/require"
11)
12
13func TestModeFrameTargets(t *testing.T) {
14 f := newVimModeTestFixture(t)
15 tests := []struct {
16 mode input.Mode
17 want layout.Widget
18 }{
19 {input.ModeVim, f.pv1A.Widget()},
20 {input.ModePane, f.pv1A.Widget()},
21 {input.ModeResize, f.pv1A.Widget()},
22 {input.ModeTab, f.app.workspaceViews["tab-1"].Widget()},
23 {input.ModeSession, f.app.workspaceViews["tab-1"].Widget()},
24 {input.ModeNormal, nil},
25 }
26 for _, tt := range tests {
27 t.Run(tt.mode.String(), func(t *testing.T) {
28 require.Equal(t, tt.want, f.app.modeFrameTarget(f.bw1, tt.mode))
29 })
30 }
31 f.bw1.tabs.ActiveTabID = "tab-missing"
32 require.Nil(t, f.app.modeFrameTarget(f.bw1, input.ModePane))
33}
34
35// The mode border is drawn by the frame's own overlay, above web content.
36// Pane widgets are only geometry targets and must never receive mode classes:
37// an inset shadow on them is painted below the web view and is invisible.
38func TestModeFrameSetTargetStylesFrameBorderNotPane(t *testing.T) {
39 first := mocks.NewMockWidget(t) // no expectations: must stay unstyled
40 second := mocks.NewMockWidget(t) // no expectations: must stay unstyled
41 border := mocks.NewMockWidget(t)
42 f := &modeFrame{borderStyle: border}
43 border.EXPECT().AddCssClass("pane-mode-active").Once()
44 f.setTarget(first, "pane-mode-active")
45 border.EXPECT().RemoveCssClass("pane-mode-active").Once()
46 for _, class := range vimPulseClasses {
47 border.EXPECT().RemoveCssClass(class).Times(2)
48 }
49 border.EXPECT().AddCssClass("resize-mode-active").Once()
50 f.setTarget(second, "resize-mode-active")
51 border.EXPECT().RemoveCssClass("resize-mode-active").Once()
52 f.setTarget(nil, "")
53}
54
55func TestModeFrameVimPulseAlternatesOnBorder(t *testing.T) {
56 pane := mocks.NewMockWidget(t) // no expectations: must stay unstyled
57 target := mocks.NewMockWidget(t)
58 f := &modeFrame{mode: input.ModeVim, target: pane, frameClass: "vim-mode-active", borderStyle: target}
59 for _, pulse := range []struct {
60 fast bool
61 class string
62 cycle string
63 }{
64 {false, "vim-mode-pulse", "vim-mode-pulse-cycle-a"},
65 {true, "vim-mode-pulse-fast", "vim-mode-pulse-cycle-b"},
66 } {
67 for _, class := range []string{"vim-mode-pulse", "vim-mode-pulse-fast", "vim-mode-pulse-cycle-a", "vim-mode-pulse-cycle-b"} {
68 target.EXPECT().RemoveCssClass(class).Once()
69 }
70 target.EXPECT().AddCssClass(pulse.class).Once()
71 target.EXPECT().AddCssClass(pulse.cycle).Once()
72 f.pulse(pulse.fast)
73 }
74 target.EXPECT().RemoveCssClass("vim-mode-active").Once()
75 for _, class := range []string{"vim-mode-pulse", "vim-mode-pulse-fast", "vim-mode-pulse-cycle-a", "vim-mode-pulse-cycle-b"} {
76 target.EXPECT().RemoveCssClass(class).Once()
77 }
78 f.setTarget(nil, "")
79}
80
81func TestModeLegendActionsUseActiveConfiguration(t *testing.T) {
82 workspace := entity.WorkspaceConfig{
83 PaneMode: entity.PaneModeConfig{Actions: map[string]entity.ActionBinding{"split-right": {Keys: []string{"r"}}}},
84 ResizeMode: entity.ResizeModeConfig{Actions: map[string]entity.ActionBinding{"resize-increase": {Keys: []string{"+"}}}},
85 VimMode: entity.VimModeConfig{Actions: map[string]entity.ActionBinding{"focus-input": {Keys: []string{"gi"}}}},
86 TabMode: entity.TabModeConfig{Actions: map[string]entity.ActionBinding{"next-tab": {Keys: []string{"l"}}}},
87 }
88 session := entity.SessionConfig{SessionMode: entity.SessionModeConfig{Actions: map[string]entity.ActionBinding{
89 "session-manager": {Keys: []string{"s"}},
90 }}}
91 for _, tc := range []struct {
92 mode input.Mode
93 class string
94 actions map[string]entity.ActionBinding
95 }{
96 {input.ModePane, "pane-mode-active", workspace.PaneMode.Actions},
97 {input.ModeResize, "resize-mode-active", workspace.ResizeMode.Actions},
98 {input.ModeVim, "vim-mode-active", workspace.VimMode.Actions},
99 {input.ModeTab, "tab-mode-active", workspace.TabMode.Actions},
100 {input.ModeSession, "session-mode-active", session.SessionMode.Actions},
101 } {
102 t.Run(tc.mode.String(), func(t *testing.T) {
103 require.Equal(t, tc.actions, legendActions(tc.mode, workspace, session))
104 require.Equal(t, tc.class, modeFrameClass(tc.mode))
105 })
106 }
107 require.Nil(t, legendActions(input.ModeNormal, workspace, session))
108 require.Empty(t, modeFrameClass(input.ModeNormal))
109 require.Equal(t, "→", displayLegendKey("arrowright"))
110 require.Equal(t, "SPLIT", modeLegendGroup(input.ModePane, "split-right"))
111 require.Equal(t, "JUMP", modeLegendGroup(input.ModeVim, "heading-next"))
112}
113
114func TestModeFramePendingSequenceFiltersBindings(t *testing.T) {
115 require.True(t, legendSequenceMatches("3]", []string{"]]"}))
116 require.False(t, legendSequenceMatches("3]", []string{"gi"}))
117 require.True(t, legendSequenceMatches("3", []string{"gi"}))
118 require.True(t, legendSequenceMatches("", []string{"gi"}))
119}
120
121func TestModeFrameTargetDoesNotDependOnGlobalFocusedWindow(t *testing.T) {
122 f := newVimModeTestFixture(t)
123 f.app.lastFocusedWindowID = f.bw2.id
124 require.Equal(t, f.pv1A.Widget(), f.app.modeFrameTarget(f.bw1, input.ModeVim))
125 require.Equal(t, f.pv2A.Widget(), f.app.modeFrameTarget(f.bw2, input.ModeVim))
126}
127
128func TestShouldLinger(t *testing.T) {
129 rect := func() (int, int, int, int, bool) { return 10, 20, 30, 40, true }
130 for _, tt := range []struct {
131 name string
132 enabled, inside bool
133 x, y float64
134 placement func() (int, int, int, int, bool)
135 want bool
136 }{
137 {"inside", true, true, 10, 20, rect, true},
138 {"edge outside", true, true, 40, 20, rect, false},
139 {"outside", true, true, 5, 25, rect, false},
140 {"disabled", false, true, 15, 25, rect, false},
141 {"left window", true, false, 15, 25, rect, false},
142 {"nil placement", true, true, 15, 25, nil, false},
143 } {
144 t.Run(tt.name, func(t *testing.T) {
145 require.Equal(t, tt.want, shouldLinger(tt.enabled, tt.inside, tt.x, tt.y, tt.placement))
146 })
147 }
148}
149
150func TestShouldStartLinger(t *testing.T) {
151 rect := func() (int, int, int, int, bool) { return 10, 20, 30, 40, true }
152 require.True(t, shouldStartLinger(true, false, true, true, 15, 25, rect))
153 require.False(t, shouldStartLinger(true, true, true, true, 15, 25, rect))
154 require.False(t, shouldStartLinger(false, false, true, true, 15, 25, rect))
155}