1package keys
2
3import (
4 "testing"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/stretchr/testify/require"
8)
9
10func TestParseKeySpecAcceptsValidSpecs(t *testing.T) {
11 cases := []struct {
12 name string
13 spec string
14 wantRunes []rune
15 wantArrow byte
16 }{
17 {name: "letter", spec: "alt+x", wantRunes: []rune{'x'}},
18 {name: "unicode single char", spec: "alt+ñ", wantRunes: []rune{'ñ'}},
19 {name: "space", spec: "alt+space", wantRunes: []rune{' '}},
20 {name: "left arrow", spec: "alt+left", wantArrow: 'D'},
21 {name: "right arrow", spec: "alt+right", wantArrow: 'C'},
22 {name: "up arrow", spec: "alt+up", wantArrow: 'A'},
23 {name: "down arrow", spec: "alt+down", wantArrow: 'B'},
24 {name: "digit includes layout aliases", spec: "alt+2", wantRunes: []rune{'2', 'é'}},
25 {name: "digit eight includes known aliases", spec: "alt+8", wantRunes: []rune{'8', '_', '!'}},
26 }
27
28 for _, tc := range cases {
29 t.Run(tc.name, func(t *testing.T) {
30 got, err := ParseKeySpec(tc.spec)
31 require.NoError(t, err)
32 require.Equal(t, tc.wantRunes, got.altRunes)
33 require.Equal(t, tc.wantArrow, got.altArrow)
34 })
35 }
36}
37
38func TestParseKeySpecRejectsMalformedSpecs(t *testing.T) {
39 cases := []string{
40 "",
41 "x",
42 "ctrl+x",
43 "alt+",
44 "alt+xy",
45 "alt+0",
46 "alt+10",
47 "alt+tab",
48 "alt+Space",
49 " alt+x",
50 "alt+x ",
51 }
52
53 for _, spec := range cases {
54 t.Run(spec, func(t *testing.T) {
55 _, err := ParseKeySpec(spec)
56 require.Error(t, err)
57 })
58 }
59}
60
61func TestActionNamesAreCanonical(t *testing.T) {
62 cases := []struct {
63 a Action
64 name string
65 }{
66 {ActionOpenPalette, "open-palette"},
67 {ActionToggleFloatingPane, "toggle-floating-pane"},
68 {ActionJumpAttention, "jump-attention"},
69 {ActionFocusPaneLeft, "focus-pane-left"},
70 {ActionFocusPaneRight, "focus-pane-right"},
71 {ActionFocusPaneUp, "focus-pane-up"},
72 {ActionFocusPaneDown, "focus-pane-down"},
73 {ActionSwitchTab1, "switch-tab-1"},
74 {ActionSwitchTab2, "switch-tab-2"},
75 {ActionSwitchTab3, "switch-tab-3"},
76 {ActionSwitchTab4, "switch-tab-4"},
77 {ActionSwitchTab5, "switch-tab-5"},
78 {ActionSwitchTab6, "switch-tab-6"},
79 {ActionSwitchTab7, "switch-tab-7"},
80 {ActionSwitchTab8, "switch-tab-8"},
81 {ActionSwitchTab9, "switch-tab-9"},
82 {ActionGrowPaneWidth, "grow-pane-width"},
83 {ActionShrinkPaneWidth, "shrink-pane-width"},
84 {ActionGrowPaneHeight, "grow-pane-height"},
85 {ActionShrinkPaneHeight, "shrink-pane-height"},
86 {ActionEqualizePanes, "equalize-panes"},
87 {ActionConsumeOrExpelPaneLeft, "consume-or-expel-pane-left"},
88 {ActionConsumeOrExpelPaneRight, "consume-or-expel-pane-right"},
89 }
90
91 for _, tc := range cases {
92 t.Run(tc.name, func(t *testing.T) {
93 require.Equal(t, tc.name, tc.a.Name())
94 got, ok := ActionByName(tc.name)
95 require.True(t, ok)
96 require.Equal(t, tc.a, got)
97 })
98 }
99
100 require.Equal(t, "unknown", Action(99).Name())
101 _, ok := ActionByName("unknown-action")
102 require.False(t, ok)
103}
104
105func TestBuildBindingsAppliesOverrides(t *testing.T) {
106 bindings, warnings := BuildBindings(map[string]string{
107 "open-palette": "alt+o",
108 "toggle-floating-pane": "alt+g",
109 "focus-pane-left": "alt+left",
110 "switch-tab-2": "alt+2",
111 })
112 require.Empty(t, warnings)
113
114 assertAltRuneAction(t, bindings, 'o', ActionOpenPalette)
115 assertAltRuneUnbound(t, bindings, ' ')
116 assertAltRuneAction(t, bindings, 'g', ActionToggleFloatingPane)
117 assertAltRuneUnbound(t, bindings, 'f')
118 assertAltArrowAction(t, bindings, 'D', ActionFocusPaneLeft)
119 assertAltRuneUnbound(t, bindings, 'h')
120 assertAltRuneAction(t, bindings, '2', ActionSwitchTab2)
121 assertAltRuneAction(t, bindings, 'é', ActionSwitchTab2)
122}
123
124func TestResizeActionsBindWithoutDefaults(t *testing.T) {
125 tests := []struct {
126 name string
127 action Action
128 key rune
129 }{
130 {"grow-pane-width", ActionGrowPaneWidth, 'w'},
131 {"shrink-pane-width", ActionShrinkPaneWidth, 's'},
132 {"grow-pane-height", ActionGrowPaneHeight, 'g'},
133 {"shrink-pane-height", ActionShrinkPaneHeight, 'r'},
134 {"equalize-panes", ActionEqualizePanes, 'e'},
135 }
136
137 defaults := DefaultBindings()
138 for _, tt := range tests {
139 t.Run(tt.name, func(t *testing.T) {
140 for _, bound := range defaults.altRunes {
141 require.NotEqual(t, tt.action, bound)
142 }
143 for _, bound := range defaults.altArrows {
144 require.NotEqual(t, tt.action, bound)
145 }
146
147 bindings, warnings := BuildBindings(map[string]string{tt.name: "alt+" + string(tt.key)})
148 require.Empty(t, warnings)
149 assertAltRuneAction(t, bindings, tt.key, tt.action)
150 })
151 }
152}
153
154func TestBuildBindingsWarnsAndSkipsInvalidEntries(t *testing.T) {
155 bindings, warnings := BuildBindings(map[string]string{
156 "missing": "alt+m",
157 "open-palette": "ctrl+o",
158 "jump-attention": "alt+j",
159 })
160
161 require.Len(t, warnings, 3)
162 warningText := warnings[0].Msg + "\n" + warnings[1].Msg + "\n" + warnings[2].Msg
163 require.Contains(t, warningText, "unknown action")
164 require.Contains(t, warningText, "invalid key")
165 require.Contains(t, warningText, "duplicate key")
166 assertAltRuneAction(t, bindings, ' ', ActionOpenPalette)
167 assertAltRuneAction(t, bindings, 'a', ActionJumpAttention)
168 assertAltRuneAction(t, bindings, 'j', ActionFocusPaneDown)
169}
170
171func TestBuildBindingsAllowsSameActionToKeepDefaultKey(t *testing.T) {
172 bindings, warnings := BuildBindings(map[string]string{
173 "focus-pane-left": "alt+h",
174 })
175
176 require.Empty(t, warnings)
177 assertAltRuneAction(t, bindings, 'h', ActionFocusPaneLeft)
178}
179
180func TestBuildBindingEntriesDuplicateKeyConflictsUseFileOrder(t *testing.T) {
181 tests := []struct {
182 name string
183 overrides []domain.ConfigEntry
184 wantAltP Action
185 wantOpenDefault bool
186 wantFocusDefault bool
187 wantWarningAction string
188 }{
189 {
190 name: "open palette first wins",
191 overrides: []domain.ConfigEntry{
192 {Key: "open-palette", Value: "alt+p"},
193 {Key: "focus-pane-left", Value: "alt+p"},
194 },
195 wantAltP: ActionOpenPalette,
196 wantFocusDefault: true,
197 wantWarningAction: "focus-pane-left",
198 },
199 {
200 name: "focus pane left first wins",
201 overrides: []domain.ConfigEntry{
202 {Key: "focus-pane-left", Value: "alt+p"},
203 {Key: "open-palette", Value: "alt+p"},
204 },
205 wantAltP: ActionFocusPaneLeft,
206 wantOpenDefault: true,
207 wantWarningAction: "open-palette",
208 },
209 }
210
211 for _, tt := range tests {
212 t.Run(tt.name, func(t *testing.T) {
213 bindings, warnings := BuildBindingEntries(tt.overrides)
214
215 require.Len(t, warnings, 1)
216 require.Contains(t, warnings[0].Msg, tt.wantWarningAction)
217 assertAltRuneAction(t, bindings, 'p', tt.wantAltP)
218 if tt.wantOpenDefault {
219 assertAltRuneAction(t, bindings, ' ', ActionOpenPalette)
220 } else {
221 assertAltRuneUnbound(t, bindings, ' ')
222 }
223 if tt.wantFocusDefault {
224 assertAltRuneAction(t, bindings, 'h', ActionFocusPaneLeft)
225 } else {
226 assertAltRuneUnbound(t, bindings, 'h')
227 }
228 })
229 }
230}
231
232func TestBuildBindingsFreesDefaultsForOverriddenActionsBeforeConflictChecks(t *testing.T) {
233 bindings, warnings := BuildBindings(map[string]string{
234 "open-palette": "alt+o",
235 "focus-pane-down": "alt+space",
236 })
237
238 require.Empty(t, warnings)
239 assertAltRuneAction(t, bindings, 'o', ActionOpenPalette)
240 assertAltRuneAction(t, bindings, ' ', ActionFocusPaneDown)
241 assertAltRuneUnbound(t, bindings, 'j')
242}
243
244func TestBuildBindingEntriesFailedOverrideRestoreDoesNotOverwriteEarlierOverride(t *testing.T) {
245 bindings, warnings := BuildBindingEntries([]domain.ConfigEntry{
246 {Key: "open-palette", Value: "alt+a"},
247 {Key: "jump-attention", Value: "alt+a"},
248 })
249
250 require.Len(t, warnings, 1)
251 assertAltRuneAction(t, bindings, 'a', ActionOpenPalette)
252 assertAltRuneUnbound(t, bindings, ' ')
253}
254
255func assertAltRuneAction(t *testing.T, bindings *Bindings, key rune, want Action) {
256 t.Helper()
257 got, ok := bindings.actionForAltRune(key)
258 require.True(t, ok)
259 require.Equal(t, want, got)
260}
261
262func assertAltRuneUnbound(t *testing.T, bindings *Bindings, key rune) {
263 t.Helper()
264 _, ok := bindings.actionForAltRune(key)
265 require.False(t, ok)
266}
267
268func assertAltArrowAction(t *testing.T, bindings *Bindings, final byte, want Action) {
269 t.Helper()
270 got, ok := bindings.actionForAltArrow(final)
271 require.True(t, ok)
272 require.Equal(t, want, got)
273}