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", "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_ToggleSystemViews(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_ToggleSystemViewsHyphenAlias(t *testing.T) {
421 tests := []struct {
422 name string
423 want Action
424 }{
425 {name: "toggle-history-systemview", want: ActionToggleHistorySystemView},
426 {name: "toggle-favorites-sidebar", want: ActionToggleFavoritesSidebar},
427 {name: "toggle-current-page-favorite", want: ActionToggleCurrentPageFavorite},
428 {name: "toggle-config-systemview", want: ActionToggleConfigSystemView},
429 }
430
431 for _, tt := range tests {
432 t.Run(tt.name, func(t *testing.T) {
433 if got := mapConfigAction(tt.name); got != tt.want {
434 t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.name, got, tt.want)
435 }
436 })
437 }
438}
439
440func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {
441 action := mapConfigAction("toggle-floating-pane")
442 if action != ActionToggleFloatingPane {
443 t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane)
444 }
445}
446
447func TestGlobalShortcutActionMap_ConsumeOrExpelHyphenAliases(t *testing.T) {
448 actions := globalShortcutActionMap()
449 tests := []struct {
450 name string
451 want Action
452 }{
453 {name: "consume-or-expel-left", want: ActionConsumeOrExpelLeft},
454 {name: "consume-or-expel-right", want: ActionConsumeOrExpelRight},
455 {name: "consume-or-expel-up", want: ActionConsumeOrExpelUp},
456 {name: "consume-or-expel-down", want: ActionConsumeOrExpelDown},
457 }
458
459 for _, tt := range tests {
460 t.Run(tt.name, func(t *testing.T) {
461 if got := actions[tt.name]; got != tt.want {
462 t.Fatalf("globalShortcutActionMap()[%s] = %s, want %s", tt.name, got, tt.want)
463 }
464 })
465 }
466}
467
468func TestNewShortcutSet_VimModeActivationShortcut(t *testing.T) {
469 workspace := &entity.WorkspaceConfig{
470 TabMode: entity.TabModeConfig{ActivationShortcut: "ctrl+t"},
471 PaneMode: entity.PaneModeConfig{ActivationShortcut: "ctrl+p"},
472 VimMode: entity.VimModeConfig{ActivationShortcut: "ctrl+y"},
473 }
474
475 set := NewShortcutSet(context.Background(), workspace, nil)
476
477 binding, ok := ParseKeyString("ctrl+y")
478 if !ok {
479 t.Fatal("failed to parse ctrl+y")
480 }
481
482 action, found := set.Global[binding]
483 if !found {
484 t.Fatal("vim mode activation shortcut not found in Global")
485 }
486 if action != ActionEnterVimMode {
487 t.Fatalf("Global shortcut action = %s, want %s", action, ActionEnterVimMode)
488 }
489}
490
491func TestShortcutSet_Lookup_VimMode(t *testing.T) {
492 set := &ShortcutSet{
493 Global: ShortcutTable{
494 {uint(gdk.KEY_q), ModCtrl}: ActionQuit,
495 },
496 VimMode: ShortcutTable{
497 {uint('h'), ModNone}: ActionVimScrollLeft,
498 {uint('j'), ModNone}: ActionVimScrollDown,
499 {uint('k'), ModNone}: ActionVimScrollUp,
500 {uint('l'), ModNone}: ActionVimScrollRight,
501 {uint(gdk.KEY_Escape), ModNone}: ActionExitMode,
502 {uint(gdk.KEY_Return), ModNone}: ActionExitMode,
503 {uint('y'), ModCtrl}: ActionEnterVimMode,
504 },
505 }
506
507 tests := []struct {
508 name string
509 binding KeyBinding
510 want Action
511 wantOk bool
512 }{
513 {
514 name: "scroll left",
515 binding: KeyBinding{uint('h'), ModNone},
516 want: ActionVimScrollLeft,
517 wantOk: true,
518 },
519 {
520 name: "scroll down",
521 binding: KeyBinding{uint('j'), ModNone},
522 want: ActionVimScrollDown,
523 wantOk: true,
524 },
525 {
526 name: "scroll up",
527 binding: KeyBinding{uint('k'), ModNone},
528 want: ActionVimScrollUp,
529 wantOk: true,
530 },
531 {
532 name: "scroll right",
533 binding: KeyBinding{uint('l'), ModNone},
534 want: ActionVimScrollRight,
535 wantOk: true,
536 },
537 {
538 name: "escape exits vim mode",
539 binding: KeyBinding{uint(gdk.KEY_Escape), ModNone},
540 want: ActionExitMode,
541 wantOk: true,
542 },
543 {
544 name: "enter exits vim mode",
545 binding: KeyBinding{uint(gdk.KEY_Return), ModNone},
546 want: ActionExitMode,
547 wantOk: true,
548 },
549 {
550 name: "activation shortcut still toggles vim mode",
551 binding: KeyBinding{uint('y'), ModCtrl},
552 want: ActionEnterVimMode,
553 wantOk: true,
554 },
555 {
556 name: "global shortcut does not fire inside vim mode",
557 binding: KeyBinding{uint(gdk.KEY_q), ModCtrl},
558 want: "",
559 wantOk: false,
560 },
561 }
562
563 for _, tt := range tests {
564 t.Run(tt.name, func(t *testing.T) {
565 got, ok := set.Lookup(tt.binding, ModeVim)
566 if ok != tt.wantOk {
567 t.Errorf("Lookup() ok = %v, want %v", ok, tt.wantOk)
568 return
569 }
570 if got != tt.want {
571 t.Errorf("Lookup() = %v, want %v", got, tt.want)
572 }
573 })
574 }
575}
576
577func TestMapConfigAction_VimScrollActions(t *testing.T) {
578 tests := []struct {
579 name string
580 config string
581 expected Action
582 }{
583 {name: "vim_scroll_left", config: "vim_scroll_left", expected: ActionVimScrollLeft},
584 {name: "vim-scroll-left", config: "vim-scroll-left", expected: ActionVimScrollLeft},
585 {name: "vim_scroll_down", config: "vim_scroll_down", expected: ActionVimScrollDown},
586 {name: "vim-scroll-down", config: "vim-scroll-down", expected: ActionVimScrollDown},
587 {name: "vim_scroll_up", config: "vim_scroll_up", expected: ActionVimScrollUp},
588 {name: "vim-scroll-up", config: "vim-scroll-up", expected: ActionVimScrollUp},
589 {name: "vim_scroll_right", config: "vim_scroll_right", expected: ActionVimScrollRight},
590 {name: "vim-scroll-right", config: "vim-scroll-right", expected: ActionVimScrollRight},
591 {name: "vim_scroll_down_fast", config: "vim_scroll_down_fast", expected: ActionVimScrollDownFast},
592 {name: "vim-scroll-down-fast", config: "vim-scroll-down-fast", expected: ActionVimScrollDownFast},
593 {name: "vim_scroll_up_fast", config: "vim_scroll_up_fast", expected: ActionVimScrollUpFast},
594 {name: "vim-scroll-up-fast", config: "vim-scroll-up-fast", expected: ActionVimScrollUpFast},
595 }
596
597 for _, tt := range tests {
598 t.Run(tt.name, func(t *testing.T) {
599 got := mapConfigAction(tt.config)
600 if got != tt.expected {
601 t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.config, got, tt.expected)
602 }
603 })
604 }
605}