gemba 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/THIRD_PARTY_NOTICES +113 -0
- data/assets/JetBrainsMonoNL-Regular.ttf +0 -0
- data/assets/ark-pixel-12px-monospaced-ja.ttf +0 -0
- data/bin/gemba +14 -0
- data/ext/gemba/extconf.rb +185 -0
- data/ext/gemba/gemba_ext.c +1051 -0
- data/ext/gemba/gemba_ext.h +15 -0
- data/gemba.gemspec +38 -0
- data/lib/gemba/child_window.rb +62 -0
- data/lib/gemba/cli.rb +384 -0
- data/lib/gemba/config.rb +621 -0
- data/lib/gemba/core.rb +121 -0
- data/lib/gemba/headless.rb +12 -0
- data/lib/gemba/headless_player.rb +206 -0
- data/lib/gemba/hotkey_map.rb +202 -0
- data/lib/gemba/input_mappings.rb +214 -0
- data/lib/gemba/locale.rb +92 -0
- data/lib/gemba/locales/en.yml +157 -0
- data/lib/gemba/locales/ja.yml +157 -0
- data/lib/gemba/method_coverage_service.rb +265 -0
- data/lib/gemba/overlay_renderer.rb +109 -0
- data/lib/gemba/player.rb +1515 -0
- data/lib/gemba/recorder.rb +156 -0
- data/lib/gemba/recorder_decoder.rb +325 -0
- data/lib/gemba/rom_info_window.rb +346 -0
- data/lib/gemba/rom_loader.rb +100 -0
- data/lib/gemba/runtime.rb +39 -0
- data/lib/gemba/save_state_manager.rb +155 -0
- data/lib/gemba/save_state_picker.rb +199 -0
- data/lib/gemba/settings_window.rb +1173 -0
- data/lib/gemba/tip_service.rb +133 -0
- data/lib/gemba/toast_overlay.rb +128 -0
- data/lib/gemba/version.rb +5 -0
- data/lib/gemba.rb +17 -0
- data/test/fixtures/test.gba +0 -0
- data/test/fixtures/test.sav +0 -0
- data/test/shared/screenshot_helper.rb +113 -0
- data/test/shared/simplecov_config.rb +59 -0
- data/test/shared/teek_test_worker.rb +388 -0
- data/test/shared/tk_test_helper.rb +354 -0
- data/test/support/input_mocks.rb +61 -0
- data/test/support/player_helpers.rb +77 -0
- data/test/test_cli.rb +281 -0
- data/test/test_config.rb +897 -0
- data/test/test_core.rb +401 -0
- data/test/test_gamepad_map.rb +116 -0
- data/test/test_headless_player.rb +205 -0
- data/test/test_helper.rb +19 -0
- data/test/test_hotkey_map.rb +396 -0
- data/test/test_keyboard_map.rb +108 -0
- data/test/test_locale.rb +159 -0
- data/test/test_mgba.rb +26 -0
- data/test/test_overlay_renderer.rb +199 -0
- data/test/test_player.rb +903 -0
- data/test/test_recorder.rb +180 -0
- data/test/test_rom_loader.rb +149 -0
- data/test/test_save_state_manager.rb +289 -0
- data/test/test_settings_hotkeys.rb +434 -0
- data/test/test_settings_window.rb +1039 -0
- data/test/test_tip_service.rb +138 -0
- data/test/test_toast_overlay.rb +216 -0
- data/test/test_virtual_keyboard.rb +39 -0
- data/test/test_xor_delta.rb +61 -0
- metadata +234 -0
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if ENV['COVERAGE']
|
|
4
|
+
require 'simplecov'
|
|
5
|
+
require_relative 'shared/simplecov_config'
|
|
6
|
+
|
|
7
|
+
coverage_name = ENV['COVERAGE_NAME'] || 'gemba'
|
|
8
|
+
SimpleCov.coverage_dir "#{SimpleCovConfig::PROJECT_ROOT}/coverage/results/#{coverage_name}"
|
|
9
|
+
SimpleCov.command_name "gemba:#{coverage_name}"
|
|
10
|
+
SimpleCov.print_error_status = false
|
|
11
|
+
SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
|
|
12
|
+
|
|
13
|
+
SimpleCov.start do
|
|
14
|
+
SimpleCovConfig.apply_filters(self)
|
|
15
|
+
track_files "#{SimpleCovConfig::PROJECT_ROOT}/lib/**/*.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require "minitest/autorun"
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "json"
|
|
6
|
+
require "set"
|
|
7
|
+
|
|
8
|
+
# Minimal config stub for unit testing HotkeyMap without Tk or SDL2.
|
|
9
|
+
class MockHotkeyConfig
|
|
10
|
+
attr_reader :hotkey_data, :saved_hotkeys
|
|
11
|
+
|
|
12
|
+
def initialize(hotkey_data = {})
|
|
13
|
+
@hotkey_data = hotkey_data
|
|
14
|
+
@saved_hotkeys = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def hotkeys
|
|
18
|
+
@hotkey_data
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_hotkey(action, hk)
|
|
22
|
+
@saved_hotkeys[action.to_s] = hk
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reload!
|
|
26
|
+
# no-op for unit tests
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class TestHotkeyMap < Minitest::Test
|
|
31
|
+
def setup
|
|
32
|
+
require "gemba/hotkey_map"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def make_map(hotkey_data = {})
|
|
36
|
+
config = MockHotkeyConfig.new(hotkey_data)
|
|
37
|
+
[Gemba::HotkeyMap.new(config), config]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# -- Defaults -------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def test_defaults_match_expected_keysyms
|
|
43
|
+
map, = make_map
|
|
44
|
+
assert_equal 'q', map.key_for(:quit)
|
|
45
|
+
assert_equal 'p', map.key_for(:pause)
|
|
46
|
+
assert_equal 'Tab', map.key_for(:fast_forward)
|
|
47
|
+
assert_equal 'F11', map.key_for(:fullscreen)
|
|
48
|
+
assert_equal 'F3', map.key_for(:show_fps)
|
|
49
|
+
assert_equal 'F5', map.key_for(:quick_save)
|
|
50
|
+
assert_equal 'F8', map.key_for(:quick_load)
|
|
51
|
+
assert_equal 'F6', map.key_for(:save_states)
|
|
52
|
+
assert_equal 'F9', map.key_for(:screenshot)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_all_actions_have_defaults
|
|
56
|
+
map, = make_map
|
|
57
|
+
Gemba::HotkeyMap::ACTIONS.each do |action|
|
|
58
|
+
refute_nil map.key_for(action), "Missing default for #{action}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# -- Reverse lookup -------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def test_action_for_returns_correct_action
|
|
65
|
+
map, = make_map
|
|
66
|
+
assert_equal :quit, map.action_for('q')
|
|
67
|
+
assert_equal :pause, map.action_for('p')
|
|
68
|
+
assert_equal :fast_forward, map.action_for('Tab')
|
|
69
|
+
assert_equal :quick_save, map.action_for('F5')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_action_for_returns_nil_for_unknown_key
|
|
73
|
+
map, = make_map
|
|
74
|
+
assert_nil map.action_for('z')
|
|
75
|
+
assert_nil map.action_for('unknown')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# -- Rebinding ------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def test_set_rebinds_action
|
|
81
|
+
map, = make_map
|
|
82
|
+
map.set(:quit, 'Escape')
|
|
83
|
+
assert_equal 'Escape', map.key_for(:quit)
|
|
84
|
+
assert_equal :quit, map.action_for('Escape')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_set_clears_conflict
|
|
88
|
+
map, = make_map
|
|
89
|
+
# 'p' is currently bound to :pause — rebind :quit to 'p'
|
|
90
|
+
map.set(:quit, 'p')
|
|
91
|
+
assert_equal 'p', map.key_for(:quit)
|
|
92
|
+
assert_nil map.key_for(:pause), "Old action should be unbound"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_set_does_not_affect_other_bindings
|
|
96
|
+
map, = make_map
|
|
97
|
+
map.set(:quit, 'Escape')
|
|
98
|
+
assert_equal 'p', map.key_for(:pause)
|
|
99
|
+
assert_equal 'F5', map.key_for(:quick_save)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# -- Reset ----------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def test_reset_restores_defaults
|
|
105
|
+
map, = make_map
|
|
106
|
+
map.set(:quit, 'Escape')
|
|
107
|
+
map.set(:pause, 'F12')
|
|
108
|
+
map.reset!
|
|
109
|
+
assert_equal 'q', map.key_for(:quit)
|
|
110
|
+
assert_equal 'p', map.key_for(:pause)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# -- Config loading -------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def test_load_config_applies_saved_bindings
|
|
116
|
+
map, = make_map({ 'quit' => 'Escape', 'pause' => 'F12' })
|
|
117
|
+
assert_equal 'Escape', map.key_for(:quit)
|
|
118
|
+
assert_equal 'F12', map.key_for(:pause)
|
|
119
|
+
# Unsaved actions keep defaults
|
|
120
|
+
assert_equal 'Tab', map.key_for(:fast_forward)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_load_config_ignores_unknown_actions
|
|
124
|
+
map, = make_map({ 'bogus' => 'F1' })
|
|
125
|
+
assert_nil map.action_for('F1')
|
|
126
|
+
assert_equal 'q', map.key_for(:quit)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_load_config_empty_uses_defaults
|
|
130
|
+
map, = make_map({})
|
|
131
|
+
assert_equal 'q', map.key_for(:quit)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# -- Save to config -------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def test_save_to_config_writes_all_bindings
|
|
137
|
+
map, config = make_map
|
|
138
|
+
map.set(:quit, 'Escape')
|
|
139
|
+
map.save_to_config
|
|
140
|
+
assert_equal 'Escape', config.saved_hotkeys['quit']
|
|
141
|
+
assert_equal 'p', config.saved_hotkeys['pause']
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_save_to_config_preserves_combo_arrays
|
|
145
|
+
map, config = make_map
|
|
146
|
+
map.set(:quit, ['Control', 'q'])
|
|
147
|
+
map.save_to_config
|
|
148
|
+
assert_equal ['Control', 'q'], config.saved_hotkeys['quit']
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# -- Labels ---------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def test_labels_returns_copy
|
|
154
|
+
map, = make_map
|
|
155
|
+
labels = map.labels
|
|
156
|
+
labels[:quit] = 'CHANGED'
|
|
157
|
+
assert_equal 'q', map.key_for(:quit), "Modifying labels hash should not affect map"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_labels_reflects_current_state
|
|
161
|
+
map, = make_map
|
|
162
|
+
map.set(:quit, 'Escape')
|
|
163
|
+
labels = map.labels
|
|
164
|
+
assert_equal 'Escape', labels[:quit]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# -- Modifier combo support -----------------------------------------------
|
|
168
|
+
|
|
169
|
+
def test_set_combo_hotkey
|
|
170
|
+
map, = make_map
|
|
171
|
+
map.set(:quit, ['Control', 'q'])
|
|
172
|
+
assert_equal ['Control', 'q'], map.key_for(:quit)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_action_for_combo_with_modifiers
|
|
176
|
+
map, = make_map
|
|
177
|
+
map.set(:quit, ['Control', 'q'])
|
|
178
|
+
result = map.action_for('q', modifiers: Set.new(['Control']))
|
|
179
|
+
assert_equal :quit, result
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_action_for_combo_wrong_modifiers
|
|
183
|
+
map, = make_map
|
|
184
|
+
map.set(:quit, ['Control', 'q'])
|
|
185
|
+
assert_nil map.action_for('q', modifiers: Set.new(['Shift']))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_action_for_combo_no_modifiers
|
|
189
|
+
map, = make_map
|
|
190
|
+
map.set(:quit, ['Control', 'q'])
|
|
191
|
+
assert_nil map.action_for('q'), "Combo should not match without modifiers"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_action_for_plain_key_ignores_active_modifiers
|
|
195
|
+
map, = make_map
|
|
196
|
+
# 'p' (pause) is a plain key — should only match without modifiers
|
|
197
|
+
assert_nil map.action_for('p', modifiers: Set.new(['Control']))
|
|
198
|
+
assert_equal :pause, map.action_for('p')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def test_action_for_multi_modifier_combo
|
|
202
|
+
map, = make_map
|
|
203
|
+
map.set(:screenshot, ['Control', 'Shift', 's'])
|
|
204
|
+
result = map.action_for('s', modifiers: Set.new(['Control', 'Shift']))
|
|
205
|
+
assert_equal :screenshot, result
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_action_for_multi_modifier_partial_match_fails
|
|
209
|
+
map, = make_map
|
|
210
|
+
map.set(:screenshot, ['Control', 'Shift', 's'])
|
|
211
|
+
assert_nil map.action_for('s', modifiers: Set.new(['Control']))
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_set_combo_clears_conflicting_combo
|
|
215
|
+
map, = make_map
|
|
216
|
+
map.set(:quit, ['Control', 'q'])
|
|
217
|
+
map.set(:pause, ['Control', 'q'])
|
|
218
|
+
assert_nil map.key_for(:quit), "Old combo should be unbound"
|
|
219
|
+
assert_equal ['Control', 'q'], map.key_for(:pause)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_load_config_with_combo_array
|
|
223
|
+
map, = make_map({ 'quit' => ['Control', 'q'] })
|
|
224
|
+
assert_equal ['Control', 'q'], map.key_for(:quit)
|
|
225
|
+
result = map.action_for('q', modifiers: Set.new(['Control']))
|
|
226
|
+
assert_equal :quit, result
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# -- normalize (class method) ---------------------------------------------
|
|
230
|
+
|
|
231
|
+
def test_normalize_plain_string
|
|
232
|
+
assert_equal 'F5', Gemba::HotkeyMap.normalize('F5')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def test_normalize_single_element_array
|
|
236
|
+
assert_equal 'q', Gemba::HotkeyMap.normalize(['q'])
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def test_normalize_sorts_modifiers_canonically
|
|
240
|
+
result = Gemba::HotkeyMap.normalize(['Shift', 'Control', 's'])
|
|
241
|
+
assert_equal ['Control', 'Shift', 's'], result
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_normalize_preserves_correct_order
|
|
245
|
+
result = Gemba::HotkeyMap.normalize(['Control', 'Shift', 's'])
|
|
246
|
+
assert_equal ['Control', 'Shift', 's'], result
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# -- display_name (class method) ------------------------------------------
|
|
250
|
+
|
|
251
|
+
def test_display_name_plain_string
|
|
252
|
+
assert_equal 'F5', Gemba::HotkeyMap.display_name('F5')
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def test_display_name_combo
|
|
256
|
+
result = Gemba::HotkeyMap.display_name(['Control', 'q'])
|
|
257
|
+
assert_equal 'Ctrl+Q', result
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def test_display_name_multi_modifier
|
|
261
|
+
result = Gemba::HotkeyMap.display_name(['Control', 'Shift', 's'])
|
|
262
|
+
assert_equal 'Ctrl+Shift+S', result
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# -- modifier helpers (class methods) -------------------------------------
|
|
266
|
+
|
|
267
|
+
def test_modifier_key_recognizes_modifiers
|
|
268
|
+
assert Gemba::HotkeyMap.modifier_key?('Control_L')
|
|
269
|
+
assert Gemba::HotkeyMap.modifier_key?('Shift_R')
|
|
270
|
+
assert Gemba::HotkeyMap.modifier_key?('Alt_L')
|
|
271
|
+
assert Gemba::HotkeyMap.modifier_key?('Meta_L')
|
|
272
|
+
assert Gemba::HotkeyMap.modifier_key?('Super_R')
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def test_modifier_key_rejects_non_modifiers
|
|
276
|
+
refute Gemba::HotkeyMap.modifier_key?('a')
|
|
277
|
+
refute Gemba::HotkeyMap.modifier_key?('F5')
|
|
278
|
+
refute Gemba::HotkeyMap.modifier_key?('Return')
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_normalize_modifier
|
|
282
|
+
assert_equal 'Control', Gemba::HotkeyMap.normalize_modifier('Control_L')
|
|
283
|
+
assert_equal 'Control', Gemba::HotkeyMap.normalize_modifier('Control_R')
|
|
284
|
+
assert_equal 'Shift', Gemba::HotkeyMap.normalize_modifier('Shift_L')
|
|
285
|
+
assert_equal 'Alt', Gemba::HotkeyMap.normalize_modifier('Alt_L')
|
|
286
|
+
assert_equal 'Alt', Gemba::HotkeyMap.normalize_modifier('Meta_L')
|
|
287
|
+
assert_nil Gemba::HotkeyMap.normalize_modifier('a')
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def test_modifiers_from_state_empty
|
|
291
|
+
result = Gemba::HotkeyMap.modifiers_from_state(0)
|
|
292
|
+
assert_empty result
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_modifiers_from_state_shift
|
|
296
|
+
result = Gemba::HotkeyMap.modifiers_from_state(1)
|
|
297
|
+
assert_equal Set.new(['Shift']), result
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def test_modifiers_from_state_control
|
|
301
|
+
result = Gemba::HotkeyMap.modifiers_from_state(4)
|
|
302
|
+
assert_equal Set.new(['Control']), result
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_modifiers_from_state_alt
|
|
306
|
+
result = Gemba::HotkeyMap.modifiers_from_state(8)
|
|
307
|
+
assert_equal Set.new(['Alt']), result
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_modifiers_from_state_control_shift
|
|
311
|
+
result = Gemba::HotkeyMap.modifiers_from_state(5) # 4|1
|
|
312
|
+
assert_equal Set.new(['Control', 'Shift']), result
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def test_action_for_empty_modifiers_set_matches_plain
|
|
316
|
+
map, = make_map
|
|
317
|
+
# Empty set should behave same as nil (match plain keys)
|
|
318
|
+
assert_equal :pause, map.action_for('p', modifiers: Set.new)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# -- Rewind action ---------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
def test_rewind_in_actions
|
|
324
|
+
assert_includes Gemba::HotkeyMap::ACTIONS, :rewind
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def test_rewind_default_is_shift_tab
|
|
328
|
+
map, = make_map
|
|
329
|
+
assert_equal ['Shift', 'Tab'], map.key_for(:rewind)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def test_rewind_action_for_shift_tab
|
|
333
|
+
map, = make_map
|
|
334
|
+
result = map.action_for('Tab', modifiers: Set.new(['Shift']))
|
|
335
|
+
assert_equal :rewind, result
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def test_rewind_does_not_match_plain_tab
|
|
339
|
+
map, = make_map
|
|
340
|
+
# Plain Tab is fast_forward, not rewind
|
|
341
|
+
assert_equal :fast_forward, map.action_for('Tab')
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def test_iso_left_tab_normalized_to_rewind
|
|
345
|
+
map, = make_map
|
|
346
|
+
# Shift+Tab produces ISO_Left_Tab on many platforms
|
|
347
|
+
result = map.action_for('ISO_Left_Tab', modifiers: Set.new(['Shift']))
|
|
348
|
+
assert_equal :rewind, result
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_normalize_keysym_iso_left_tab
|
|
352
|
+
assert_equal 'Tab', Gemba::HotkeyMap.normalize_keysym('ISO_Left_Tab')
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def test_normalize_keysym_uppercase_letter
|
|
356
|
+
assert_equal 'q', Gemba::HotkeyMap.normalize_keysym('Q')
|
|
357
|
+
assert_equal 's', Gemba::HotkeyMap.normalize_keysym('S')
|
|
358
|
+
assert_equal 'a', Gemba::HotkeyMap.normalize_keysym('A')
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def test_normalize_keysym_shifted_numbers
|
|
362
|
+
assert_equal '1', Gemba::HotkeyMap.normalize_keysym('exclam')
|
|
363
|
+
assert_equal '2', Gemba::HotkeyMap.normalize_keysym('at')
|
|
364
|
+
assert_equal '0', Gemba::HotkeyMap.normalize_keysym('parenright')
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def test_normalize_keysym_passthrough
|
|
368
|
+
assert_equal 'q', Gemba::HotkeyMap.normalize_keysym('q')
|
|
369
|
+
assert_equal 'F5', Gemba::HotkeyMap.normalize_keysym('F5')
|
|
370
|
+
assert_equal 'Tab', Gemba::HotkeyMap.normalize_keysym('Tab')
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def test_action_for_shift_uppercase_matches_plain_combo
|
|
374
|
+
map, = make_map
|
|
375
|
+
map.set(:screenshot, ['Shift', 's'])
|
|
376
|
+
# Tk sends 'S' (uppercase) when Shift+s is pressed
|
|
377
|
+
result = map.action_for('S', modifiers: Set.new(['Shift']))
|
|
378
|
+
assert_equal :screenshot, result
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# -- Record action ---------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
def test_record_in_actions
|
|
384
|
+
assert_includes Gemba::HotkeyMap::ACTIONS, :record
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def test_record_default_is_f10
|
|
388
|
+
map, = make_map
|
|
389
|
+
assert_equal 'F10', map.key_for(:record)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def test_record_dispatches_on_f10
|
|
393
|
+
map, = make_map
|
|
394
|
+
assert_equal :record, map.action_for('F10')
|
|
395
|
+
end
|
|
396
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba"
|
|
5
|
+
require_relative "../lib/gemba/config"
|
|
6
|
+
require_relative "../lib/gemba/input_mappings"
|
|
7
|
+
require_relative "support/input_mocks"
|
|
8
|
+
|
|
9
|
+
class TestKeyboardMap < Minitest::Test
|
|
10
|
+
def setup
|
|
11
|
+
@config = MockInputConfig.new
|
|
12
|
+
@map = Gemba::KeyboardMap.new(@config)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_default_labels
|
|
16
|
+
labels = @map.labels
|
|
17
|
+
assert_equal 'z', labels[:a]
|
|
18
|
+
assert_equal 'x', labels[:b]
|
|
19
|
+
assert_equal 'Return', labels[:start]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_set_remap
|
|
23
|
+
@map.set(:a, 'q')
|
|
24
|
+
assert_equal 'q', @map.labels[:a]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_set_removes_old_binding
|
|
28
|
+
@map.set(:a, 'q')
|
|
29
|
+
refute @map.labels.values.include?('z')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_set_unknown_button
|
|
33
|
+
@map.set(:nonexistent, 'q')
|
|
34
|
+
assert_equal 'z', @map.labels[:a]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_reset
|
|
38
|
+
@map.set(:a, 'q')
|
|
39
|
+
@map.reset!
|
|
40
|
+
assert_equal 'z', @map.labels[:a]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_mask_no_device
|
|
44
|
+
assert_equal 0, @map.mask
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_mask_with_device
|
|
48
|
+
kb = Gemba::VirtualKeyboard.new
|
|
49
|
+
@map.device = kb
|
|
50
|
+
kb.press('z')
|
|
51
|
+
mask = @map.mask
|
|
52
|
+
assert_equal Gemba::KEY_A, mask & Gemba::KEY_A
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_mask_multiple_keys
|
|
56
|
+
kb = Gemba::VirtualKeyboard.new
|
|
57
|
+
@map.device = kb
|
|
58
|
+
kb.press('z')
|
|
59
|
+
kb.press('x')
|
|
60
|
+
mask = @map.mask
|
|
61
|
+
assert_equal Gemba::KEY_A, mask & Gemba::KEY_A
|
|
62
|
+
assert_equal Gemba::KEY_B, mask & Gemba::KEY_B
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_mask_released_key_not_in_mask
|
|
66
|
+
kb = Gemba::VirtualKeyboard.new
|
|
67
|
+
@map.device = kb
|
|
68
|
+
kb.press('z')
|
|
69
|
+
kb.release('z')
|
|
70
|
+
assert_equal 0, @map.mask
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_supports_deadzone
|
|
74
|
+
refute @map.supports_deadzone?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_dead_zone_pct
|
|
78
|
+
assert_equal 0, @map.dead_zone_pct
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_set_dead_zone_raises
|
|
82
|
+
assert_raises(NotImplementedError) { @map.set_dead_zone(100) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_load_config
|
|
86
|
+
cfg = MockInputConfig.new(keyboard_mappings: { 'a' => 'q', 'b' => 'w' })
|
|
87
|
+
map = Gemba::KeyboardMap.new(cfg)
|
|
88
|
+
assert_equal 'q', map.labels[:a]
|
|
89
|
+
assert_equal 'w', map.labels[:b]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_reload
|
|
93
|
+
@map.set(:a, 'q')
|
|
94
|
+
@map.reload!
|
|
95
|
+
assert @config.calls.any? { |c| c[0] == :reload! }
|
|
96
|
+
# After reload with empty config, defaults restored
|
|
97
|
+
assert_equal 'z', @map.labels[:a]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_save_to_config
|
|
101
|
+
@map.save_to_config
|
|
102
|
+
set_calls = @config.calls.select { |c| c[0] == :set_mapping }
|
|
103
|
+
assert_equal 10, set_calls.size
|
|
104
|
+
a_call = set_calls.find { |c| c[2] == :a }
|
|
105
|
+
assert_equal 'keyboard', a_call[1]
|
|
106
|
+
assert_equal 'z', a_call[3]
|
|
107
|
+
end
|
|
108
|
+
end
|
data/test/test_locale.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "../lib/gemba/locale"
|
|
6
|
+
|
|
7
|
+
class TestMGBALocale < Minitest::Test
|
|
8
|
+
# -- Loading ---------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
def test_load_english
|
|
11
|
+
Gemba::Locale.load('en')
|
|
12
|
+
assert_equal 'en', Gemba::Locale.language
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_load_japanese
|
|
16
|
+
Gemba::Locale.load('ja')
|
|
17
|
+
assert_equal 'ja', Gemba::Locale.language
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_fallback_to_english_for_unknown_locale
|
|
21
|
+
Gemba::Locale.load('zz')
|
|
22
|
+
# Should still load (fell back to en.yml) and return translations
|
|
23
|
+
assert_equal 'File', Gemba::Locale.translate('menu.file')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_load_auto_detects_from_env
|
|
27
|
+
original = ENV['LANG']
|
|
28
|
+
ENV['LANG'] = 'ja_JP.UTF-8'
|
|
29
|
+
Gemba::Locale.load
|
|
30
|
+
assert_equal 'ja', Gemba::Locale.language
|
|
31
|
+
ensure
|
|
32
|
+
ENV['LANG'] = original
|
|
33
|
+
Gemba::Locale.load('en')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_load_auto_string_treated_as_auto_detect
|
|
37
|
+
original = ENV['LANG']
|
|
38
|
+
ENV['LANG'] = 'en_US.UTF-8'
|
|
39
|
+
Gemba::Locale.load('auto')
|
|
40
|
+
assert_equal 'en', Gemba::Locale.language
|
|
41
|
+
ensure
|
|
42
|
+
ENV['LANG'] = original
|
|
43
|
+
Gemba::Locale.load('en')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# -- Translation -----------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def test_translate_english_string
|
|
49
|
+
Gemba::Locale.load('en')
|
|
50
|
+
assert_equal 'File', Gemba::Locale.translate('menu.file')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_translate_japanese_string
|
|
54
|
+
Gemba::Locale.load('ja')
|
|
55
|
+
assert_equal 'ファイル', Gemba::Locale.translate('menu.file')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_translate_nested_key
|
|
59
|
+
Gemba::Locale.load('en')
|
|
60
|
+
assert_equal 'Video', Gemba::Locale.translate('settings.video')
|
|
61
|
+
assert_equal 'ROM Info', Gemba::Locale.translate('rom_info.title')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_translate_with_interpolation
|
|
65
|
+
Gemba::Locale.load('en')
|
|
66
|
+
result = Gemba::Locale.translate('toast.state_saved', slot: 3)
|
|
67
|
+
assert_equal 'State saved to slot 3', result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_translate_with_multiple_vars
|
|
71
|
+
Gemba::Locale.load('en')
|
|
72
|
+
# dialog.game_running_msg has {name}
|
|
73
|
+
result = Gemba::Locale.translate('dialog.game_running_msg', name: 'Zelda')
|
|
74
|
+
assert_equal 'Another game is running. Switch to Zelda?', result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_translate_japanese_with_interpolation
|
|
78
|
+
Gemba::Locale.load('ja')
|
|
79
|
+
result = Gemba::Locale.translate('toast.state_saved', slot: 5)
|
|
80
|
+
assert_includes result, '5'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_translate_missing_key_returns_key
|
|
84
|
+
Gemba::Locale.load('en')
|
|
85
|
+
assert_equal 'nonexistent.key', Gemba::Locale.translate('nonexistent.key')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_translate_partial_key_returns_key
|
|
89
|
+
Gemba::Locale.load('en')
|
|
90
|
+
# 'menu' exists but is a Hash, not a string
|
|
91
|
+
assert_equal 'menu', Gemba::Locale.translate('menu')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# -- Alias -----------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def test_t_alias
|
|
97
|
+
Gemba::Locale.load('en')
|
|
98
|
+
assert_equal 'File', Gemba::Locale.t('menu.file')
|
|
99
|
+
assert_equal Gemba::Locale.translate('menu.file'),
|
|
100
|
+
Gemba::Locale.t('menu.file')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# -- Available languages ---------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def test_available_languages
|
|
106
|
+
langs = Gemba::Locale.available_languages
|
|
107
|
+
assert_includes langs, 'en'
|
|
108
|
+
assert_includes langs, 'ja'
|
|
109
|
+
assert_equal langs, langs.sort, 'should be sorted'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# -- Translatable mixin ----------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def test_translatable_mixin
|
|
115
|
+
klass = Class.new { include Gemba::Locale::Translatable; public :translate, :t }
|
|
116
|
+
obj = klass.new
|
|
117
|
+
Gemba::Locale.load('en')
|
|
118
|
+
assert_equal 'File', obj.translate('menu.file')
|
|
119
|
+
assert_equal 'File', obj.t('menu.file')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_translatable_mixin_with_interpolation
|
|
123
|
+
klass = Class.new { include Gemba::Locale::Translatable; public :translate }
|
|
124
|
+
obj = klass.new
|
|
125
|
+
Gemba::Locale.load('en')
|
|
126
|
+
assert_equal 'State saved to slot 7', obj.translate('toast.state_saved', slot: 7)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# -- Completeness ----------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def test_en_and_ja_have_same_keys
|
|
132
|
+
en_path = File.expand_path('../lib/gemba/locales/en.yml', __dir__)
|
|
133
|
+
ja_path = File.expand_path('../lib/gemba/locales/ja.yml', __dir__)
|
|
134
|
+
en = YAML.safe_load_file(en_path)
|
|
135
|
+
ja = YAML.safe_load_file(ja_path)
|
|
136
|
+
|
|
137
|
+
en_keys = flatten_keys(en)
|
|
138
|
+
ja_keys = flatten_keys(ja)
|
|
139
|
+
|
|
140
|
+
missing_in_ja = en_keys - ja_keys
|
|
141
|
+
missing_in_en = ja_keys - en_keys
|
|
142
|
+
|
|
143
|
+
assert_empty missing_in_ja, "Keys in en.yml missing from ja.yml: #{missing_in_ja.join(', ')}"
|
|
144
|
+
assert_empty missing_in_en, "Keys in ja.yml missing from en.yml: #{missing_in_en.join(', ')}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def flatten_keys(hash, prefix = nil)
|
|
150
|
+
hash.flat_map do |key, value|
|
|
151
|
+
full_key = prefix ? "#{prefix}.#{key}" : key.to_s
|
|
152
|
+
if value.is_a?(Hash)
|
|
153
|
+
flatten_keys(value, full_key)
|
|
154
|
+
else
|
|
155
|
+
[full_key]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/test/test_mgba.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba"
|
|
5
|
+
|
|
6
|
+
class TestMGBA < Minitest::Test
|
|
7
|
+
def test_version_constant
|
|
8
|
+
assert_match(/\A\d+\.\d+\.\d+\z/, Gemba::VERSION)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_module_structure
|
|
12
|
+
assert_kind_of Module, Gemba
|
|
13
|
+
assert_equal Class, Gemba::Core.class
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_key_constants_are_unique_powers_of_two
|
|
17
|
+
keys = %i[KEY_A KEY_B KEY_SELECT KEY_START
|
|
18
|
+
KEY_RIGHT KEY_LEFT KEY_UP KEY_DOWN KEY_R KEY_L]
|
|
19
|
+
|
|
20
|
+
values = keys.map { |k| Gemba.const_get(k) }
|
|
21
|
+
assert_equal values.size, values.uniq.size, "all key constants should be unique"
|
|
22
|
+
values.each do |v|
|
|
23
|
+
assert_equal 0, v & (v - 1), "#{v} should be a power of 2"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|