1package input
2
3import (
4 "fmt"
5 "testing"
6)
7
8// TestKeycodeConstants verifies the hardware keycode constants are correct.
9// These are XKB keycodes (evdev + 8) for the number row keys.
10func TestKeycodeConstants(t *testing.T) {
11 tests := []struct {
12 name string
13 keycode uint
14 expected uint
15 }{
16 {"KeycodeDigit1", KeycodeDigit1, 10},
17 {"KeycodeDigit2", KeycodeDigit2, 11},
18 {"KeycodeDigit3", KeycodeDigit3, 12},
19 {"KeycodeDigit4", KeycodeDigit4, 13},
20 {"KeycodeDigit5", KeycodeDigit5, 14},
21 {"KeycodeDigit6", KeycodeDigit6, 15},
22 {"KeycodeDigit7", KeycodeDigit7, 16},
23 {"KeycodeDigit8", KeycodeDigit8, 17},
24 {"KeycodeDigit9", KeycodeDigit9, 18},
25 {"KeycodeDigit0", KeycodeDigit0, 19},
26 }
27
28 for _, tt := range tests {
29 t.Run(tt.name, func(t *testing.T) {
30 if tt.keycode != tt.expected {
31 t.Errorf("%s = %d, want %d", tt.name, tt.keycode, tt.expected)
32 }
33 })
34 }
35}
36
37// TestKeycodeToTabActionMapping verifies all number row keycodes map to the correct tab actions.
38func TestKeycodeToTabActionMapping(t *testing.T) {
39 tests := []struct {
40 keycode uint
41 expectedAction Action
42 }{
43 {KeycodeDigit1, ActionSwitchTabIndex1},
44 {KeycodeDigit2, ActionSwitchTabIndex2},
45 {KeycodeDigit3, ActionSwitchTabIndex3},
46 {KeycodeDigit4, ActionSwitchTabIndex4},
47 {KeycodeDigit5, ActionSwitchTabIndex5},
48 {KeycodeDigit6, ActionSwitchTabIndex6},
49 {KeycodeDigit7, ActionSwitchTabIndex7},
50 {KeycodeDigit8, ActionSwitchTabIndex8},
51 {KeycodeDigit9, ActionSwitchTabIndex9},
52 {KeycodeDigit0, ActionSwitchTabIndex10},
53 }
54
55 for _, tt := range tests {
56 t.Run(string(tt.expectedAction), func(t *testing.T) {
57 action, ok := KeycodeToTabAction[tt.keycode]
58 if !ok {
59 t.Errorf("keycode %d not found in KeycodeToTabAction map", tt.keycode)
60 return
61 }
62 if action != tt.expectedAction {
63 t.Errorf("KeycodeToTabAction[%d] = %s, want %s", tt.keycode, action, tt.expectedAction)
64 }
65 })
66 }
67}
68
69// TestKeycodeToTabActionCompleteness verifies the map contains exactly 10 entries.
70func TestKeycodeToTabActionCompleteness(t *testing.T) {
71 expectedCount := 10
72 if len(KeycodeToTabAction) != expectedCount {
73 t.Errorf("KeycodeToTabAction has %d entries, want %d", len(KeycodeToTabAction), expectedCount)
74 }
75}
76
77// TestKeycodeToTabActionNoUnexpectedKeycodes verifies only valid keycodes are in the map.
78func TestKeycodeToTabActionNoUnexpectedKeycodes(t *testing.T) {
79 validKeycodes := map[uint]bool{
80 KeycodeDigit1: true,
81 KeycodeDigit2: true,
82 KeycodeDigit3: true,
83 KeycodeDigit4: true,
84 KeycodeDigit5: true,
85 KeycodeDigit6: true,
86 KeycodeDigit7: true,
87 KeycodeDigit8: true,
88 KeycodeDigit9: true,
89 KeycodeDigit0: true,
90 }
91
92 for keycode := range KeycodeToTabAction {
93 if !validKeycodes[keycode] {
94 t.Errorf("unexpected keycode %d in KeycodeToTabAction map", keycode)
95 }
96 }
97}
98
99// European keyboard layout test scenarios.
100// These test the concept that hardware keycodes are layout-independent.
101//
102// On all these layouts, the physical "1" key has hardware keycode 10,
103// regardless of what character it produces:
104//
105// | Layout | Physical "1" key produces | Keycode |
106// |-------------|---------------------------|---------|
107// | QWERTY (US) | 1 | 10 |
108// | QWERTY (UK) | 1 | 10 |
109// | AZERTY (FR) | & | 10 |
110// | AZERTY (BE) | & | 10 |
111// | QWERTZ (DE) | 1 | 10 |
112// | QWERTZ (CH) | 1 | 10 |
113// | Dvorak | 1 | 10 |
114
115// TestEuropeanLayoutKeycodes documents and verifies the keycode behavior
116// expected for various European keyboard layouts.
117func TestEuropeanLayoutKeycodes(t *testing.T) {
118 // This test documents that our solution works for all these layouts
119 // because we use hardware keycodes, not keyvals.
120 //
121 // The keycode for the physical number row is always 10-19,
122 // regardless of what character the key produces.
123
124 layouts := []struct {
125 name string
126 description string
127 // Physical key position (1-9, 0) always maps to keycode 10-19
128 // The keyval differs but we don't care - we use keycode
129 }{
130 {
131 name: "QWERTY_US",
132 description: "US English - 1234567890 produce digits directly",
133 },
134 {
135 name: "QWERTY_UK",
136 description: "UK English - 1234567890 produce digits directly",
137 },
138 {
139 name: "AZERTY_FR",
140 description: "French - &é\"'(-è_çà produce symbols, Shift for digits",
141 },
142 {
143 name: "AZERTY_BE",
144 description: "Belgian - &é\"'(§è!çà produce symbols, Shift for digits",
145 },
146 {
147 name: "QWERTZ_DE",
148 description: "German - 1234567890 produce digits directly (symbols on Shift)",
149 },
150 {
151 name: "QWERTZ_CH",
152 description: "Swiss German - 1234567890 produce digits directly",
153 },
154 {
155 name: "QWERTZ_AT",
156 description: "Austrian - 1234567890 produce digits directly",
157 },
158 {
159 name: "Dvorak",
160 description: "Dvorak - number row same as QWERTY",
161 },
162 {
163 name: "Colemak",
164 description: "Colemak - number row same as QWERTY",
165 },
166 {
167 name: "Portuguese_PT",
168 description: "Portuguese - 1234567890 produce digits directly",
169 },
170 {
171 name: "Spanish_ES",
172 description: "Spanish - 1234567890 produce digits directly",
173 },
174 {
175 name: "Italian_IT",
176 description: "Italian - 1234567890 produce digits directly",
177 },
178 {
179 name: "Nordic",
180 description: "Nordic layouts (SE/NO/DK/FI) - 1234567890 produce digits directly",
181 },
182 }
183
184 for _, layout := range layouts {
185 t.Run(layout.name, func(t *testing.T) {
186 // For all layouts, pressing Alt + physical "1" key should:
187 // 1. Have hardware keycode 10 (KeycodeDigit1)
188 // 2. Map to ActionSwitchTabIndex1
189
190 action, ok := KeycodeToTabAction[KeycodeDigit1]
191 if !ok {
192 t.Errorf("[%s] keycode 10 (physical '1') not mapped", layout.name)
193 return
194 }
195 if action != ActionSwitchTabIndex1 {
196 t.Errorf("[%s] keycode 10 maps to %s, want %s",
197 layout.name, action, ActionSwitchTabIndex1)
198 }
199
200 // Document the layout for clarity
201 t.Logf("Layout %s: %s - keycode fallback will work", layout.name, layout.description)
202 })
203 }
204}
205
206// TestAZERTYSpecificScenario tests the specific scenario from issue #25.
207// On French AZERTY, pressing Alt+1 produces Alt+& (ampersand) because
208// the digit 1 requires Shift. Our keycode fallback handles this.
209func TestAZERTYSpecificScenario(t *testing.T) {
210 // Scenario: User on French AZERTY keyboard presses Alt + physical "1" key
211 //
212 // What happens:
213 // 1. GTK receives: keyval=ampersand (0x26), keycode=10, modifiers=Alt
214 // 2. keyval lookup for "Alt+&" fails (no such shortcut defined)
215 // 3. Keycode fallback: keycode 10 with Alt modifier
216 // 4. KeycodeToTabAction[10] = ActionSwitchTabIndex1
217 // 5. Tab 1 is activated
218
219 // The keycode for physical "1" on ALL layouts
220 physicalKey1Keycode := uint(10)
221
222 // Verify our mapping handles this
223 action, ok := KeycodeToTabAction[physicalKey1Keycode]
224 if !ok {
225 t.Fatal("keycode 10 should be mapped for AZERTY support")
226 }
227 if action != ActionSwitchTabIndex1 {
228 t.Errorf("keycode 10 should map to ActionSwitchTabIndex1, got %s", action)
229 }
230}
231
232// TestNonNumberRowKeycodes verifies that non-number-row keycodes don't accidentally match.
233func TestNonNumberRowKeycodes(t *testing.T) {
234 // Some keycodes that should NOT be in our map
235 invalidKeycodes := []uint{
236 0, // Invalid
237 1, // Escape
238 9, // Before number row
239 20, // After number row (minus key)
240 24, // Q key
241 38, // A key
242 50, // Shift
243 64, // Alt
244 }
245
246 for _, keycode := range invalidKeycodes {
247 if _, ok := KeycodeToTabAction[keycode]; ok {
248 t.Errorf("keycode %d should not be in KeycodeToTabAction map", keycode)
249 }
250 }
251}
252
253// TestKeycodeToDigitIndex verifies the keycode to digit index conversion.
254func TestKeycodeToDigitIndex(t *testing.T) {
255 tests := []struct {
256 keycode uint
257 expectedIndex int
258 expectedOk bool
259 }{
260 {KeycodeDigit1, 0, true},
261 {KeycodeDigit2, 1, true},
262 {KeycodeDigit3, 2, true},
263 {KeycodeDigit4, 3, true},
264 {KeycodeDigit5, 4, true},
265 {KeycodeDigit6, 5, true},
266 {KeycodeDigit7, 6, true},
267 {KeycodeDigit8, 7, true},
268 {KeycodeDigit9, 8, true},
269 {KeycodeDigit0, 9, true},
270 // Invalid keycodes
271 {0, -1, false},
272 {9, -1, false}, // Before number row
273 {20, -1, false}, // After number row
274 {64, -1, false}, // Alt key
275 }
276
277 for _, tt := range tests {
278 t.Run(fmt.Sprintf("keycode_%d", tt.keycode), func(t *testing.T) {
279 index, ok := KeycodeToDigitIndex(tt.keycode)
280 if ok != tt.expectedOk {
281 t.Errorf("KeycodeToDigitIndex(%d) ok = %v, want %v", tt.keycode, ok, tt.expectedOk)
282 }
283 if index != tt.expectedIndex {
284 t.Errorf("KeycodeToDigitIndex(%d) index = %d, want %d", tt.keycode, index, tt.expectedIndex)
285 }
286 })
287 }
288}
289
290// TestKeycodeToDigitIndexForOmnibox verifies the function works for omnibox Ctrl+1-9/0 shortcuts.
291func TestKeycodeToDigitIndexForOmnibox(t *testing.T) {
292 // Simulate omnibox usage: Ctrl+1 should select first item (index 0)
293 // Ctrl+0 should select 10th item (index 9)
294
295 // Physical "1" key -> index 0 (first omnibox result)
296 index, ok := KeycodeToDigitIndex(KeycodeDigit1)
297 if !ok || index != 0 {
298 t.Errorf("Ctrl+1 should map to index 0, got index=%d ok=%v", index, ok)
299 }
300
301 // Physical "0" key -> index 9 (10th omnibox result)
302 index, ok = KeycodeToDigitIndex(KeycodeDigit0)
303 if !ok || index != 9 {
304 t.Errorf("Ctrl+0 should map to index 9, got index=%d ok=%v", index, ok)
305 }
306}