1package input
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/dumber/internal/domain/entity"
8 "github.com/bnema/puregotk/v4/gdk"
9)
10
11func TestParseKeyString_SingleKeys(t *testing.T) {
12 tests := []struct {
13 name string
14 input string
15 want KeyBinding
16 wantOk bool
17 }{
18 {
19 name: "escape",
20 input: "escape",
21 want: KeyBinding{Keyval: uint(gdk.KEY_Escape), Modifiers: ModNone},
22 wantOk: true,
23 },
24 {
25 name: "esc alias",
26 input: "esc",
27 want: KeyBinding{Keyval: uint(gdk.KEY_Escape), Modifiers: ModNone},
28 wantOk: true,
29 },
30 {
31 name: "return",
32 input: "return",
33 want: KeyBinding{Keyval: uint(gdk.KEY_Return), Modifiers: ModNone},
34 wantOk: true,
35 },
36 {
37 name: "enter alias",
38 input: "enter",
39 want: KeyBinding{Keyval: uint(gdk.KEY_Return), Modifiers: ModNone},
40 wantOk: true,
41 },
42 {
43 name: "tab",
44 input: "tab",
45 want: KeyBinding{Keyval: uint(gdk.KEY_Tab), Modifiers: ModNone},
46 wantOk: true,
47 },
48 {
49 name: "space",
50 input: "space",
51 want: KeyBinding{Keyval: uint(gdk.KEY_space), Modifiers: ModNone},
52 wantOk: true,
53 },
54 {
55 name: "plus symbol",
56 input: "+",
57 want: KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModNone},
58 wantOk: true,
59 },
60 {
61 name: "f5",
62 input: "f5",
63 want: KeyBinding{Keyval: uint(gdk.KEY_F5), Modifiers: ModNone},
64 wantOk: true,
65 },
66 {
67 name: "f12",
68 input: "f12",
69 want: KeyBinding{Keyval: uint(gdk.KEY_F12), Modifiers: ModNone},
70 wantOk: true,
71 },
72 {
73 name: "single letter",
74 input: "t",
75 want: KeyBinding{Keyval: uint('t'), Modifiers: ModNone},
76 wantOk: true,
77 },
78 {
79 name: "single number",
80 input: "0",
81 want: KeyBinding{Keyval: uint(gdk.KEY_0), Modifiers: ModNone},
82 wantOk: true,
83 },
84 {
85 name: "arrow left",
86 input: "left",
87 want: KeyBinding{Keyval: uint(gdk.KEY_Left), Modifiers: ModNone},
88 wantOk: true,
89 },
90 {
91 name: "arrow right",
92 input: "right",
93 want: KeyBinding{Keyval: uint(gdk.KEY_Right), Modifiers: ModNone},
94 wantOk: true,
95 },
96 }
97
98 for _, tt := range tests {
99 t.Run(tt.name, func(t *testing.T) {
100 got, ok := ParseKeyString(tt.input)
101 if ok != tt.wantOk {
102 t.Errorf("ParseKeyString(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
103 return
104 }
105 if !tt.wantOk {
106 return
107 }
108 if got.Keyval != tt.want.Keyval {
109 t.Errorf("ParseKeyString(%q) keyval = %d, want %d", tt.input, got.Keyval, tt.want.Keyval)
110 }
111 if got.Modifiers != tt.want.Modifiers {
112 t.Errorf("ParseKeyString(%q) modifiers = %d, want %d", tt.input, got.Modifiers, tt.want.Modifiers)
113 }
114 })
115 }
116}
117
118func TestParseKeyString_WithModifiers(t *testing.T) {
119 tests := []struct {
120 name string
121 input string
122 want KeyBinding
123 wantOk bool
124 }{
125 {
126 name: "ctrl+t",
127 input: "ctrl+t",
128 want: KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl},
129 wantOk: true,
130 },
131 {
132 name: "control+t",
133 input: "control+t",
134 want: KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl},
135 wantOk: true,
136 },
137 {
138 name: "shift+tab",
139 input: "shift+tab",
140 want: KeyBinding{Keyval: uint(gdk.KEY_Tab), Modifiers: ModShift},
141 wantOk: true,
142 },
143 {
144 name: "alt+left",
145 input: "alt+left",
146 want: KeyBinding{Keyval: uint(gdk.KEY_Left), Modifiers: ModAlt},
147 wantOk: true,
148 },
149 {
150 name: "ctrl+shift+t",
151 input: "ctrl+shift+t",
152 want: KeyBinding{Keyval: uint('t'), Modifiers: ModCtrl | ModShift},
153 wantOk: true,
154 },
155 {
156 name: "ctrl+r",
157 input: "ctrl+r",
158 want: KeyBinding{Keyval: uint('r'), Modifiers: ModCtrl},
159 wantOk: true,
160 },
161 {
162 name: "ctrl++",
163 input: "ctrl++",
164 want: KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModCtrl},
165 wantOk: true,
166 },
167 }
168
169 for _, tt := range tests {
170 t.Run(tt.name, func(t *testing.T) {
171 got, ok := ParseKeyString(tt.input)
172 if ok != tt.wantOk {
173 t.Errorf("ParseKeyString(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
174 return
175 }
176 if !tt.wantOk {
177 return
178 }
179 if got.Keyval != tt.want.Keyval {
180 t.Errorf("ParseKeyString(%q) keyval = %d, want %d", tt.input, got.Keyval, tt.want.Keyval)
181 }
182 if got.Modifiers != tt.want.Modifiers {
183 t.Errorf("ParseKeyString(%q) modifiers = %d, want %d", tt.input, got.Modifiers, tt.want.Modifiers)
184 }
185 })
186 }
187}
188
189func TestParseKeyString_Invalid(t *testing.T) {
190 tests := []string{
191 "",
192 "ctrl+",
193 "unknownkey",
194 "ctrl+unknownkey",
195 }
196
197 for _, input := range tests {
198 t.Run(input, func(t *testing.T) {
199 _, ok := ParseKeyString(input)
200 if ok {
201 t.Errorf("ParseKeyString(%q) should have failed", input)
202 }
203 })
204 }
205}
206
207func TestParseKeyString_CaseInsensitive(t *testing.T) {
208 tests := []struct {
209 input1 string
210 input2 string
211 }{
212 // Modifiers and named keys are case-insensitive.
213 {"ctrl+t", "CTRL+t"},
214 {"Escape", "escape"},
215 {"Tab", "TAB"},
216 // If shift is explicitly present, the key letter case shouldn't matter.
217 {"Ctrl+Shift+T", "ctrl+shift+t"},
218 }
219
220 for _, tt := range tests {
221 t.Run(tt.input1+" vs "+tt.input2, func(t *testing.T) {
222 got1, ok1 := ParseKeyString(tt.input1)
223 got2, ok2 := ParseKeyString(tt.input2)
224
225 if ok1 != ok2 {
226 t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, ok1, tt.input2, ok2)
227 return
228 }
229
230 if got1 != got2 {
231 t.Errorf("ParseKeyString case sensitivity: %q=%v, %q=%v", tt.input1, got1, tt.input2, got2)
232 }
233 })
234 }
235}
236
237func TestParseKeyString_UppercaseSingleLetterAddsShift(t *testing.T) {
238 got1, ok1 := ParseKeyString("M")
239 got2, ok2 := ParseKeyString("shift+m")
240 if !ok1 || !ok2 {
241 t.Fatalf("ParseKeyString should succeed for M and shift+m")
242 }
243 if got1 != got2 {
244 t.Fatalf("ParseKeyString uppercase should imply shift: got %v want %v", got1, got2)
245 }
246}
247
248func TestShortcutSet_Lookup(t *testing.T) {
249 set := &ShortcutSet{
250 Global: ShortcutTable{
251 {uint(gdk.KEY_q), ModCtrl}: ActionQuit,
252 {uint(gdk.KEY_l), ModCtrl}: ActionOpenOmnibox,
253 },
254 TabMode: ShortcutTable{
255 {uint('n'), ModNone}: ActionNewTab,
256 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
257 },
258 PaneMode: ShortcutTable{
259 {uint('x'), ModNone}: ActionClosePane,
260 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
261 },
262 }
263
264 tests := []struct {
265 name string
266 binding KeyBinding
267 mode Mode
268 want Action
269 wantOk bool
270 }{
271 {
272 name: "global in normal mode",
273 binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
274 mode: ModeNormal,
275 want: ActionQuit,
276 wantOk: true,
277 },
278 {
279 name: "global in tab mode",
280 binding: KeyBinding{uint(gdk.KEY_l), ModCtrl},
281 mode: ModeTab,
282 want: ActionOpenOmnibox,
283 wantOk: true,
284 },
285 {
286 name: "tab mode action",
287 binding: KeyBinding{uint('n'), ModNone},
288 mode: ModeTab,
289 want: ActionNewTab,
290 wantOk: true,
291 },
292 {
293 name: "pane mode action",
294 binding: KeyBinding{uint('x'), ModNone},
295 mode: ModePane,
296 want: ActionClosePane,
297 wantOk: true,
298 },
299 {
300 name: "escape exits tab mode",
301 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
302 mode: ModeTab,
303 want: ActionExitMode,
304 wantOk: true,
305 },
306 {
307 name: "escape exits pane mode",
308 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
309 mode: ModePane,
310 want: ActionExitMode,
311 wantOk: true,
312 },
313 {
314 name: "unknown key in normal mode",
315 binding: KeyBinding{uint('z'), ModNone},
316 mode: ModeNormal,
317 want: "",
318 wantOk: false,
319 },
320 {
321 name: "tab action not in normal mode",
322 binding: KeyBinding{uint('n'), ModNone},
323 mode: ModeNormal,
324 want: "",
325 wantOk: false,
326 },
327 }
328
329 for _, tt := range tests {
330 t.Run(tt.name, func(t *testing.T) {
331 got, ok := set.Lookup(tt.binding, tt.mode)
332 if ok != tt.wantOk {
333 t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
334 return
335 }
336 if got != tt.want {
337 t.Errorf("Lookup() = %v, want %v", got, tt.want)
338 }
339 })
340 }
341}
342
343func TestShouldAutoExitMode(t *testing.T) {
344 exitActions := []Action{
345 ActionNewTab,
346 ActionCloseTab,
347 ActionRenameTab,
348 ActionSplitRight,
349 ActionSplitLeft,
350 ActionSplitUp,
351 ActionSplitDown,
352 ActionClosePane,
353 ActionStackPane,
354 ActionMovePaneToTab,
355 ActionMovePaneToNextTab,
356 ActionEjectPaneToWindow,
357 }
358
359 stayActions := []Action{
360 ActionNextTab,
361 ActionPreviousTab,
362 ActionFocusRight,
363 ActionFocusLeft,
364 ActionGoBack,
365 ActionZoomIn,
366 ActionOpenOmnibox,
367 }
368
369 for _, action := range exitActions {
370 if !ShouldAutoExitMode(action) {
371 t.Errorf("ShouldAutoExitMode(%s) = false, want true", action)
372 }
373 }
374
375 for _, action := range stayActions {
376 if ShouldAutoExitMode(action) {
377 t.Errorf("ShouldAutoExitMode(%s) = true, want false", action)
378 }
379 }
380}
381
382func TestMapConfigAction_EjectPaneToWindowAliases(t *testing.T) {
383 tests := []string{"eject-pane-to-window"}
384 for _, name := range tests {
385 t.Run(name, func(t *testing.T) {
386 if got := mapConfigAction(name); got != ActionEjectPaneToWindow {
387 t.Fatalf("mapConfigAction(%s) = %s, want %s", name, got, ActionEjectPaneToWindow)
388 }
389 })
390 }
391}
392
393func TestMapConfigAction_ToggleFloatingPane(t *testing.T) {
394 action := mapConfigAction("toggle-floating-pane")
395 if action != ActionToggleFloatingPane {
396 t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane)
397 }
398}
399
400func TestMapConfigAction_ToggleSystemViewsHyphenAlias(t *testing.T) {
401 tests := []struct {
402 name string
403 want Action
404 }{
405 {name: "toggle-history-systemview", want: ActionToggleHistorySystemView},
406 {name: "toggle-favorites-sidebar", want: ActionToggleFavoritesSidebar},
407 {name: "toggle-current-page-favorite", want: ActionToggleCurrentPageFavorite},
408 {name: "toggle-config-systemview", want: ActionToggleConfigSystemView},
409 }
410
411 for _, tt := range tests {
412 t.Run(tt.name, func(t *testing.T) {
413 if got := mapConfigAction(tt.name); got != tt.want {
414 t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.name, got, tt.want)
415 }
416 })
417 }
418}
419
420func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {
421 action := mapConfigAction("toggle-floating-pane")
422 if action != ActionToggleFloatingPane {
423 t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane)
424 }
425}
426
427func TestGlobalShortcutActionMap_ConsumeOrExpelHyphenAliases(t *testing.T) {
428 actions := globalShortcutActionMap()
429 tests := []struct {
430 name string
431 want Action
432 }{
433 {name: "consume-or-expel-left", want: ActionConsumeOrExpelLeft},
434 {name: "consume-or-expel-right", want: ActionConsumeOrExpelRight},
435 {name: "consume-or-expel-up", want: ActionConsumeOrExpelUp},
436 {name: "consume-or-expel-down", want: ActionConsumeOrExpelDown},
437 }
438
439 for _, tt := range tests {
440 t.Run(tt.name, func(t *testing.T) {
441 if got := actions[tt.name]; got != tt.want {
442 t.Fatalf("globalShortcutActionMap()[%s] = %s, want %s", tt.name, got, tt.want)
443 }
444 })
445 }
446}
447
448func TestNewShortcutSet_VimModeActivationShortcut(t *testing.T) {
449 workspace := &entity.WorkspaceConfig{
450 TabMode: entity.TabModeConfig{ActivationShortcut: "ctrl+t"},
451 PaneMode: entity.PaneModeConfig{ActivationShortcut: "ctrl+p"},
452 VimMode: entity.VimModeConfig{ActivationShortcut: "ctrl+y"},
453 }
454
455 set := NewShortcutSet(context.Background(), workspace, nil)
456
457 binding, ok := ParseKeyString("ctrl+y")
458 if !ok {
459 t.Fatal("failed to parse ctrl+y")
460 }
461
462 action, found := set.Global[binding]
463 if !found {
464 t.Fatal("vim mode activation shortcut not found in Global")
465 }
466 if action != ActionEnterVimMode {
467 t.Fatalf("Global shortcut action = %s, want %s", action, ActionEnterVimMode)
468 }
469}
470
471func TestShortcutSet_Lookup_VimMode(t *testing.T) {
472 set := &ShortcutSet{
473 Global: ShortcutTable{
474 {uint(gdk.KEY_q), ModCtrl}: ActionQuit,
475 },
476 VimMode: ShortcutTable{
477 {uint('h'), ModNone}: ActionVimScrollLeft,
478 {uint('j'), ModNone}: ActionVimScrollDown,
479 {uint('k'), ModNone}: ActionVimScrollUp,
480 {uint('l'), ModNone}: ActionVimScrollRight,
481 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
482 {uint(gdk.KEY_Return), ModNone}: ActionExitMode,
483 {uint('y'), ModCtrl}: ActionEnterVimMode,
484 },
485 }
486
487 tests := []struct {
488 name string
489 binding KeyBinding
490 want Action
491 wantOk bool
492 }{
493 {
494 name: "scroll left",
495 binding: KeyBinding{uint('h'), ModNone},
496 want: ActionVimScrollLeft,
497 wantOk: true,
498 },
499 {
500 name: "scroll down",
501 binding: KeyBinding{uint('j'), ModNone},
502 want: ActionVimScrollDown,
503 wantOk: true,
504 },
505 {
506 name: "scroll up",
507 binding: KeyBinding{uint('k'), ModNone},
508 want: ActionVimScrollUp,
509 wantOk: true,
510 },
511 {
512 name: "scroll right",
513 binding: KeyBinding{uint('l'), ModNone},
514 want: ActionVimScrollRight,
515 wantOk: true,
516 },
517 {
518 name: "escape exits vim mode",
519 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
520 want: ActionExitMode,
521 wantOk: true,
522 },
523 {
524 name: "enter exits vim mode",
525 binding: KeyBinding{uint(gdk.KEY_Return), ModNone},
526 want: ActionExitMode,
527 wantOk: true,
528 },
529 {
530 name: "activation shortcut still toggles vim mode",
531 binding: KeyBinding{uint('y'), ModCtrl},
532 want: ActionEnterVimMode,
533 wantOk: true,
534 },
535 {
536 name: "global shortcut does not fire inside vim mode",
537 binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
538 want: "",
539 wantOk: false,
540 },
541 }
542
543 for _, tt := range tests {
544 t.Run(tt.name, func(t *testing.T) {
545 got, ok := set.Lookup(tt.binding, ModeVim)
546 if ok != tt.wantOk {
547 t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
548 return
549 }
550 if got != tt.want {
551 t.Errorf("Lookup() = %v, want %v", got, tt.want)
552 }
553 })
554 }
555}
556
557func TestMapConfigAction_VimScrollActions(t *testing.T) {
558 tests := []struct {
559 name string
560 config string
561 expected Action
562 }{
563 {name: "vim-scroll-left", config: "vim-scroll-left", expected: ActionVimScrollLeft},
564 {name: "vim-scroll-down", config: "vim-scroll-down", expected: ActionVimScrollDown},
565 {name: "vim-scroll-up", config: "vim-scroll-up", expected: ActionVimScrollUp},
566 {name: "vim-scroll-right", config: "vim-scroll-right", expected: ActionVimScrollRight},
567 {name: "vim-scroll-down-fast", config: "vim-scroll-down-fast", expected: ActionVimScrollDownFast},
568 {name: "vim-scroll-up-fast", config: "vim-scroll-up-fast", expected: ActionVimScrollUpFast},
569 }
570
571 for _, tt := range tests {
572 t.Run(tt.name, func(t *testing.T) {
573 got := mapConfigAction(tt.config)
574 if got != tt.expected {
575 t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.config, got, tt.expected)
576 }
577 })
578 }
579}