1package input
2
3import (
4 "testing"
5
6 "github.com/jwijenbergh/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 }
355
356 stayActions := []Action{
357 ActionNextTab,
358 ActionPreviousTab,
359 ActionFocusRight,
360 ActionFocusLeft,
361 ActionGoBack,
362 ActionZoomIn,
363 ActionOpenOmnibox,
364 }
365
366 for _, action := range exitActions {
367 if !ShouldAutoExitMode(action) {
368 t.Errorf("ShouldAutoExitMode(%s) = false, want true", action)
369 }
370 }
371
372 for _, action := range stayActions {
373 if ShouldAutoExitMode(action) {
374 t.Errorf("ShouldAutoExitMode(%s) = true, want false", action)
375 }
376 }
377}
378
379func TestMapConfigAction_ToggleFloatingPane(t *testing.T) {
380 action := mapConfigAction("toggle_floating_pane")
381 if action != ActionToggleFloatingPane {
382 t.Fatalf("mapConfigAction(toggle_floating_pane) = %s, want %s", action, ActionToggleFloatingPane)
383 }
384}
385
386func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {
387 action := mapConfigAction("toggle-floating-pane")
388 if action != ActionToggleFloatingPane {
389 t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane)
390 }
391}