1package input
2
3import (
4 "testing"
5
6 "github.com/bnema/puregotk/v4/gdk"
7)
8
9func TestParseKeyString_SingleKeys(t *testing.T) {
10 tests := []struct {
11 name string
12 input string
13 want KeyBinding
14 wantOk bool
15 }{
16 {
17 name: "escape",
18 input: "escape",
19 want: KeyBinding{Keyval: uint(gdk.KEY_Escape), Modifiers: ModNone},
20 wantOk: true,
21 },
22 {
23 name: "esc alias",
24 input: "esc",
25 want: KeyBinding{Keyval: uint(gdk.KEY_Escape), Modifiers: ModNone},
26 wantOk: true,
27 },
28 {
29 name: "return",
30 input: "return",
31 want: KeyBinding{Keyval: uint(gdk.KEY_Return), Modifiers: ModNone},
32 wantOk: true,
33 },
34 {
35 name: "enter alias",
36 input: "enter",
37 want: KeyBinding{Keyval: uint(gdk.KEY_Return), Modifiers: ModNone},
38 wantOk: true,
39 },
40 {
41 name: "tab",
42 input: "tab",
43 want: KeyBinding{Keyval: uint(gdk.KEY_Tab), Modifiers: ModNone},
44 wantOk: true,
45 },
46 {
47 name: "space",
48 input: "space",
49 want: KeyBinding{Keyval: uint(gdk.KEY_space), Modifiers: ModNone},
50 wantOk: true,
51 },
52 {
53 name: "plus symbol",
54 input: "+",
55 want: KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModNone},
56 wantOk: true,
57 },
58 {
59 name: "f5",
60 input: "f5",
61 want: KeyBinding{Keyval: uint(gdk.KEY_F5), Modifiers: ModNone},
62 wantOk: true,
63 },
64 {
65 name: "f12",
66 input: "f12",
67 want: KeyBinding{Keyval: uint(gdk.KEY_F12), Modifiers: ModNone},
68 wantOk: true,
69 },
70 {
71 name: "single letter",
72 input: "t",
73 want: KeyBinding{Keyval: uint('t'), Modifiers: ModNone},
74 wantOk: true,
75 },
76 {
77 name: "single number",
78 input: "0",
79 want: KeyBinding{Keyval: uint(gdk.KEY_0), Modifiers: ModNone},
80 wantOk: true,
81 },
82 {
83 name: "arrow left",
84 input: "left",
85 want: KeyBinding{Keyval: uint(gdk.KEY_Left), Modifiers: ModNone},
86 wantOk: true,
87 },
88 {
89 name: "arrow right",
90 input: "right",
91 want: KeyBinding{Keyval: uint(gdk.KEY_Right), Modifiers: ModNone},
92 wantOk: true,
93 },
94 }
95
96 for _, tt := range tests {
97 t.Run(tt.name, func(t *testing.T) {
98 got, ok := ParseKeyString(tt.input)
99 if ok != tt.wantOk {
100 t.Errorf("ParseKeyString(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
101 return
102 }
103 if !tt.wantOk {
104 return
105 }
106 if got.Keyval != tt.want.Keyval {
107 t.Errorf("ParseKeyString(%q) keyval = %d, want %d", tt.input, got.Keyval, tt.want.Keyval)
108 }
109 if got.Modifiers != tt.want.Modifiers {
110 t.Errorf("ParseKeyString(%q) modifiers = %d, want %d", tt.input, got.Modifiers, tt.want.Modifiers)
111 }
112 })
113 }
114}
115
116func TestParseKeyString_WithModifiers(t *testing.T) {
117 tests := []struct {
118 name string
119 input string
120 want KeyBinding
121 wantOk bool
122 }{
123 {
124 name: "ctrl+t",
125 input: "ctrl+t",
126 want: KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl},
127 wantOk: true,
128 },
129 {
130 name: "control+t",
131 input: "control+t",
132 want: KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl},
133 wantOk: true,
134 },
135 {
136 name: "shift+tab",
137 input: "shift+tab",
138 want: KeyBinding{Keyval: uint(gdk.KEY_Tab), Modifiers: ModShift},
139 wantOk: true,
140 },
141 {
142 name: "alt+left",
143 input: "alt+left",
144 want: KeyBinding{Keyval: uint(gdk.KEY_Left), Modifiers: ModAlt},
145 wantOk: true,
146 },
147 {
148 name: "ctrl+shift+t",
149 input: "ctrl+shift+t",
150 want: KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl | ModShift},
151 wantOk: true,
152 },
153 {
154 name: "ctrl+r",
155 input: "ctrl+r",
156 want: KeyBinding{Keyval: uint('r'), Modifiers: ModCtrl},
157 wantOk: true,
158 },
159 {
160 name: "ctrl++",
161 input: "ctrl++",
162 want: KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModCtrl},
163 wantOk: true,
164 },
165 }
166
167 for _, tt := range tests {
168 t.Run(tt.name, func(t *testing.T) {
169 got, ok := ParseKeyString(tt.input)
170 if ok != tt.wantOk {
171 t.Errorf("ParseKeyString(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
172 return
173 }
174 if !tt.wantOk {
175 return
176 }
177 if got.Keyval != tt.want.Keyval {
178 t.Errorf("ParseKeyString(%q) keyval = %d, want %d", tt.input, got.Keyval, tt.want.Keyval)
179 }
180 if got.Modifiers != tt.want.Modifiers {
181 t.Errorf("ParseKeyString(%q) modifiers = %d, want %d", tt.input, got.Modifiers, tt.want.Modifiers)
182 }
183 })
184 }
185}
186
187func TestParseKeyString_Invalid(t *testing.T) {
188 tests := []string{
189 "",
190 "ctrl+",
191 "unknownkey",
192 "ctrl+unknownkey",
193 }
194
195 for _, input := range tests {
196 t.Run(input, func(t *testing.T) {
197 _, ok := ParseKeyString(input)
198 if ok {
199 t.Errorf("ParseKeyString(%q) should have failed", input)
200 }
201 })
202 }
203}
204
205func TestParseKeyString_CaseInsensitive(t *testing.T) {
206 tests := []struct {
207 input1 string
208 input2 string
209 }{
210 // Modifiers and named keys are case-insensitive.
211 {"ctrl+t", "CTRL+t"},
212 {"Escape", "escape"},
213 {"Tab", "TAB"},
214 // If shift is explicitly present, the key letter case shouldn't matter.
215 {"Ctrl+Shift+T", "ctrl+shift+t"},
216 }
217
218 for _, tt := range tests {
219 t.Run(tt.input1+" vs "+tt.input2, func(t *testing.T) {
220 got1, ok1 := ParseKeyString(tt.input1)
221 got2, ok2 := ParseKeyString(tt.input2)
222
223 if ok1 != ok2 {
224 t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, ok1, tt.input2, ok2)
225 return
226 }
227
228 if got1 != got2 {
229 t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, got1, tt.input2, got2)
230 }
231 })
232 }
233}
234
235func TestParseKeyString_UppercaseSingleLetterAddsShift(t *testing.T) {
236 got1, ok1 := ParseKeyString("M")
237 got2, ok2 := ParseKeyString("shift+m")
238 if !ok1 || !ok2 {
239 t.Fatalf("ParseKeyString should succeed for M and shift+m")
240 }
241 if got1 != got2 {
242 t.Fatalf("ParseKeyString uppercase should imply shift: got %v want %v", got1, got2)
243 }
244}
245
246func TestShortcutSet_Lookup(t *testing.T) {
247 set := &ShortcutSet{
248 Global: ShortcutTable{
249 {uint(gdk.KEY_q), ModCtrl}: ActionQuit,
250 {uint(gdk.KEY_l), ModCtrl}: ActionOpenOmnibox,
251 },
252 TabMode: ShortcutTable{
253 {uint('n'), ModNone}: ActionNewTab,
254 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
255 },
256 PaneMode: ShortcutTable{
257 {uint('x'), ModNone}: ActionClosePane,
258 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
259 },
260 }
261
262 tests := []struct {
263 name string
264 binding KeyBinding
265 mode Mode
266 want Action
267 wantOk bool
268 }{
269 {
270 name: "global in normal mode",
271 binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
272 mode: ModeNormal,
273 want: ActionQuit,
274 wantOk: true,
275 },
276 {
277 name: "global in tab mode",
278 binding: KeyBinding{uint(gdk.KEY_l), ModCtrl},
279 mode: ModeTab,
280 want: ActionOpenOmnibox,
281 wantOk: true,
282 },
283 {
284 name: "tab mode action",
285 binding: KeyBinding{uint('n'), ModNone},
286 mode: ModeTab,
287 want: ActionNewTab,
288 wantOk: true,
289 },
290 {
291 name: "pane mode action",
292 binding: KeyBinding{uint('x'), ModNone},
293 mode: ModePane,
294 want: ActionClosePane,
295 wantOk: true,
296 },
297 {
298 name: "escape exits tab mode",
299 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
300 mode: ModeTab,
301 want: ActionExitMode,
302 wantOk: true,
303 },
304 {
305 name: "escape exits pane mode",
306 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
307 mode: ModePane,
308 want: ActionExitMode,
309 wantOk: true,
310 },
311 {
312 name: "unknown key in normal mode",
313 binding: KeyBinding{uint('z'), ModNone},
314 mode: ModeNormal,
315 want: "",
316 wantOk: false,
317 },
318 {
319 name: "tab action not in normal mode",
320 binding: KeyBinding{uint('n'), ModNone},
321 mode: ModeNormal,
322 want: "",
323 wantOk: false,
324 },
325 }
326
327 for _, tt := range tests {
328 t.Run(tt.name, func(t *testing.T) {
329 got, ok := set.Lookup(tt.binding, tt.mode)
330 if ok != tt.wantOk {
331 t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
332 return
333 }
334 if got != tt.want {
335 t.Errorf("Lookup() = %v, want %v", got, tt.want)
336 }
337 })
338 }
339}
340
341func TestShouldAutoExitMode(t *testing.T) {
342 exitActions := []Action{
343 ActionNewTab,
344 ActionCloseTab,
345 ActionRenameTab,
346 ActionSplitRight,
347 ActionSplitLeft,
348 ActionSplitUp,
349 ActionSplitDown,
350 ActionClosePane,
351 ActionStackPane,
352 ActionMovePaneToTab,
353 ActionMovePaneToNextTab,
354 ActionEjectPaneToWindow,
355 }
356
357 stayActions := []Action{
358 ActionNextTab,
359 ActionPreviousTab,
360 ActionFocusRight,
361 ActionFocusLeft,
362 ActionGoBack,
363 ActionZoomIn,
364 ActionOpenOmnibox,
365 }
366
367 for _, action := range exitActions {
368 if !ShouldAutoExitMode(action) {
369 t.Errorf("ShouldAutoExitMode(%s) = false, want true", action)
370 }
371 }
372
373 for _, action := range stayActions {
374 if ShouldAutoExitMode(action) {
375 t.Errorf("ShouldAutoExitMode(%s) = true, want false", action)
376 }
377 }
378}
379
380func TestMapConfigAction_EjectPaneToWindowAliases(t *testing.T) {
381 tests := []string{"eject_pane_to_window", "eject-pane-to-window"}
382 for _, name := range tests {
383 t.Run(name, func(t *testing.T) {
384 if got := mapConfigAction(name); got != ActionEjectPaneToWindow {
385 t.Fatalf("mapConfigAction(%s) = %s, want %s", name, got, ActionEjectPaneToWindow)
386 }
387 })
388 }
389}
390
391func TestMapConfigAction_ToggleFloatingPane(t *testing.T) {
392 action := mapConfigAction("toggle_floating_pane")
393 if action != ActionToggleFloatingPane {
394 t.Fatalf("mapConfigAction(toggle_floating_pane) = %s, want %s", action, ActionToggleFloatingPane)
395 }
396}
397
398func TestMapConfigAction_ToggleSystemViews(t *testing.T) {
399 tests := []struct {
400 name string
401 want Action
402 }{
403 {name: "toggle_history_systemview", want: ActionToggleHistorySystemView},
404 {name: "toggle_favorites_systemview", want: ActionToggleFavoritesSystemView},
405 {name: "toggle_config_systemview", want: ActionToggleConfigSystemView},
406 }
407
408 for _, tt := range tests {
409 t.Run(tt.name, func(t *testing.T) {
410 if got := mapConfigAction(tt.name); got != tt.want {
411 t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.name, got, tt.want)
412 }
413 })
414 }
415}
416
417func TestMapConfigAction_ToggleSystemViewsHyphenAlias(t *testing.T) {
418 tests := []struct {
419 name string
420 want Action
421 }{
422 {name: "toggle-history-systemview", want: ActionToggleHistorySystemView},
423 {name: "toggle-favorites-systemview", want: ActionToggleFavoritesSystemView},
424 {name: "toggle-config-systemview", want: ActionToggleConfigSystemView},
425 }
426
427 for _, tt := range tests {
428 t.Run(tt.name, func(t *testing.T) {
429 if got := mapConfigAction(tt.name); got != tt.want {
430 t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.name, got, tt.want)
431 }
432 })
433 }
434}
435
436func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {
437 action := mapConfigAction("toggle-floating-pane")
438 if action != ActionToggleFloatingPane {
439 t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane)
440 }
441}
442
443func TestGlobalShortcutActionMap_ConsumeOrExpelHyphenAliases(t *testing.T) {
444 actions := globalShortcutActionMap()
445 tests := []struct {
446 name string
447 want Action
448 }{
449 {name: "consume-or-expel-left", want: ActionConsumeOrExpelLeft},
450 {name: "consume-or-expel-right", want: ActionConsumeOrExpelRight},
451 {name: "consume-or-expel-up", want: ActionConsumeOrExpelUp},
452 {name: "consume-or-expel-down", want: ActionConsumeOrExpelDown},
453 }
454
455 for _, tt := range tests {
456 t.Run(tt.name, func(t *testing.T) {
457 if got := actions[tt.name]; got != tt.want {
458 t.Fatalf("globalShortcutActionMap()[%s] = %s, want %s", tt.name, got, tt.want)
459 }
460 })
461 }
462}