gemba 0.1.1 → 0.2.1

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.
Files changed (289) hide show
  1. checksums.yaml +4 -4
  2. data/THIRD_PARTY_NOTICES +37 -2
  3. data/assets/placeholder_boxart.png +0 -0
  4. data/bin/gemba +2 -2
  5. data/ext/gemba/extconf.rb +23 -1
  6. data/ext/gemba/gemba_ext.c +436 -2
  7. data/ext/gemba/gemba_ext.h +2 -0
  8. data/gemba.gemspec +5 -3
  9. data/lib/gemba/achievements/achievement.rb +23 -0
  10. data/lib/gemba/achievements/backend.rb +190 -0
  11. data/lib/gemba/achievements/cache.rb +70 -0
  12. data/lib/gemba/achievements/credentials_presenter.rb +142 -0
  13. data/lib/gemba/achievements/fake_backend.rb +205 -0
  14. data/lib/gemba/achievements/null_backend.rb +11 -0
  15. data/lib/gemba/achievements/offline_backend.rb +168 -0
  16. data/lib/gemba/achievements/retro_achievements/backend.rb +511 -0
  17. data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
  18. data/lib/gemba/achievements/retro_achievements/ping_worker.rb +28 -0
  19. data/lib/gemba/achievements/retro_achievements/unlock_retry_worker.rb +35 -0
  20. data/lib/gemba/achievements.rb +19 -0
  21. data/lib/gemba/achievements_window.rb +556 -0
  22. data/lib/gemba/app_controller.rb +1036 -0
  23. data/lib/gemba/bios.rb +54 -0
  24. data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
  25. data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
  26. data/lib/gemba/boxart_fetcher.rb +79 -0
  27. data/lib/gemba/bus_emitter.rb +13 -0
  28. data/lib/gemba/child_window.rb +24 -1
  29. data/lib/gemba/cli/commands/config_cmd.rb +83 -0
  30. data/lib/gemba/cli/commands/decode.rb +154 -0
  31. data/lib/gemba/cli/commands/patch.rb +78 -0
  32. data/lib/gemba/cli/commands/play.rb +78 -0
  33. data/lib/gemba/cli/commands/record.rb +114 -0
  34. data/lib/gemba/cli/commands/replay.rb +161 -0
  35. data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
  36. data/lib/gemba/cli/commands/version.rb +22 -0
  37. data/lib/gemba/cli.rb +52 -364
  38. data/lib/gemba/config.rb +154 -1
  39. data/lib/gemba/data/gb_games.json +1 -0
  40. data/lib/gemba/data/gb_md5.json +1 -0
  41. data/lib/gemba/data/gba_games.json +1 -0
  42. data/lib/gemba/data/gba_md5.json +1 -0
  43. data/lib/gemba/data/gbc_games.json +1 -0
  44. data/lib/gemba/data/gbc_md5.json +1 -0
  45. data/lib/gemba/emulator_frame.rb +1084 -0
  46. data/lib/gemba/event_bus.rb +48 -0
  47. data/lib/gemba/frame_stack.rb +70 -0
  48. data/lib/gemba/game_index.rb +84 -0
  49. data/lib/gemba/game_picker_frame.rb +309 -0
  50. data/lib/gemba/gamepad_map.rb +103 -0
  51. data/lib/gemba/headless.rb +6 -5
  52. data/lib/gemba/headless_player.rb +33 -3
  53. data/lib/gemba/help_window.rb +61 -0
  54. data/lib/gemba/hotkey_map.rb +3 -1
  55. data/lib/gemba/input_recorder.rb +107 -0
  56. data/lib/gemba/input_replayer.rb +119 -0
  57. data/lib/gemba/keyboard_map.rb +90 -0
  58. data/lib/gemba/list_picker_frame.rb +271 -0
  59. data/lib/gemba/locales/en.yml +109 -5
  60. data/lib/gemba/locales/ja.yml +109 -5
  61. data/lib/gemba/main_window.rb +56 -0
  62. data/lib/gemba/modal_stack.rb +81 -0
  63. data/lib/gemba/patcher_window.rb +223 -0
  64. data/lib/gemba/platform/gb.rb +21 -0
  65. data/lib/gemba/platform/gba.rb +21 -0
  66. data/lib/gemba/platform/gbc.rb +23 -0
  67. data/lib/gemba/platform.rb +20 -0
  68. data/lib/gemba/platform_open.rb +19 -0
  69. data/lib/gemba/recorder.rb +4 -3
  70. data/lib/gemba/replay_player.rb +691 -0
  71. data/lib/gemba/rom_info.rb +57 -0
  72. data/lib/gemba/rom_info_window.rb +16 -3
  73. data/lib/gemba/rom_library.rb +106 -0
  74. data/lib/gemba/rom_overrides.rb +47 -0
  75. data/lib/gemba/rom_patcher/bps.rb +161 -0
  76. data/lib/gemba/rom_patcher/ips.rb +101 -0
  77. data/lib/gemba/rom_patcher/ups.rb +119 -0
  78. data/lib/gemba/rom_patcher.rb +109 -0
  79. data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
  80. data/lib/gemba/runtime.rb +59 -26
  81. data/lib/gemba/save_state_manager.rb +4 -7
  82. data/lib/gemba/save_state_picker.rb +17 -4
  83. data/lib/gemba/session_logger.rb +64 -0
  84. data/lib/gemba/settings/audio_tab.rb +77 -0
  85. data/lib/gemba/settings/gamepad_tab.rb +351 -0
  86. data/lib/gemba/settings/hotkeys_tab.rb +259 -0
  87. data/lib/gemba/settings/paths.rb +11 -0
  88. data/lib/gemba/settings/recording_tab.rb +83 -0
  89. data/lib/gemba/settings/save_states_tab.rb +91 -0
  90. data/lib/gemba/settings/system_tab.rb +377 -0
  91. data/lib/gemba/settings/video_tab.rb +318 -0
  92. data/lib/gemba/settings_window.rb +162 -1036
  93. data/lib/gemba/version.rb +1 -1
  94. data/lib/gemba/virtual_keyboard.rb +19 -0
  95. data/lib/gemba.rb +2 -12
  96. data/test/achievements_window/test_bulk_sync.rb +218 -0
  97. data/test/achievements_window/test_bus_events.rb +125 -0
  98. data/test/achievements_window/test_close_confirmation.rb +201 -0
  99. data/test/achievements_window/test_initial_state.rb +164 -0
  100. data/test/achievements_window/test_sorting.rb +227 -0
  101. data/test/achievements_window/test_tree_rendering.rb +133 -0
  102. data/test/fixtures/fake_bios.bin +0 -0
  103. data/test/fixtures/pong.gba +0 -0
  104. data/test/fixtures/test.gb +0 -0
  105. data/test/fixtures/test.gbc +0 -0
  106. data/test/fixtures/test_quicksave.ss +0 -0
  107. data/test/screenshots/no_focus.png +0 -0
  108. data/test/shared/teek_test_worker.rb +17 -1
  109. data/test/shared/tk_test_helper.rb +92 -4
  110. data/test/support/achievements_window_helpers.rb +18 -0
  111. data/test/support/fake_core.rb +25 -0
  112. data/test/support/fake_ra_runtime.rb +74 -0
  113. data/test/support/fake_requester.rb +78 -0
  114. data/test/support/player_helpers.rb +20 -5
  115. data/test/test_achievement.rb +32 -0
  116. data/test/{test_player.rb → test_app_controller.rb} +353 -85
  117. data/test/test_bios.rb +123 -0
  118. data/test/test_boxart_fetcher.rb +150 -0
  119. data/test/test_cli.rb +17 -265
  120. data/test/test_cli_config.rb +64 -0
  121. data/test/test_cli_decode.rb +97 -0
  122. data/test/test_cli_patch.rb +58 -0
  123. data/test/test_cli_play.rb +213 -0
  124. data/test/test_cli_ra.rb +175 -0
  125. data/test/test_cli_record.rb +69 -0
  126. data/test/test_cli_replay.rb +72 -0
  127. data/test/test_cli_sync_requester.rb +152 -0
  128. data/test/test_cli_version.rb +27 -0
  129. data/test/test_config.rb +3 -3
  130. data/test/test_config_ra.rb +69 -0
  131. data/test/test_core.rb +62 -1
  132. data/test/test_credentials_presenter.rb +192 -0
  133. data/test/test_event_bus.rb +100 -0
  134. data/test/test_fake_backend_achievements.rb +130 -0
  135. data/test/test_fake_backend_auth.rb +68 -0
  136. data/test/test_game_index.rb +77 -0
  137. data/test/test_game_picker_frame.rb +310 -0
  138. data/test/test_gamepad_map.rb +1 -3
  139. data/test/test_headless_player.rb +17 -3
  140. data/test/test_help_window.rb +82 -0
  141. data/test/test_hotkey_map.rb +22 -1
  142. data/test/test_input_recorder.rb +179 -0
  143. data/test/test_input_replay_determinism.rb +113 -0
  144. data/test/test_input_replayer.rb +162 -0
  145. data/test/test_keyboard_map.rb +1 -3
  146. data/test/test_libretro_backend.rb +41 -0
  147. data/test/test_list_picker_frame.rb +391 -0
  148. data/test/test_locale.rb +1 -1
  149. data/test/test_logging.rb +123 -0
  150. data/test/test_null_backend.rb +42 -0
  151. data/test/test_offline_backend.rb +116 -0
  152. data/test/test_overlay_renderer.rb +1 -1
  153. data/test/test_platform.rb +149 -0
  154. data/test/test_ra_backend.rb +313 -0
  155. data/test/test_ra_backend_unlock_gate.rb +56 -0
  156. data/test/test_ra_backend_unlock_retry.rb +123 -0
  157. data/test/test_recorder.rb +0 -3
  158. data/test/test_replay_player.rb +316 -0
  159. data/test/test_rom_info.rb +149 -0
  160. data/test/test_rom_overrides.rb +86 -0
  161. data/test/test_rom_patcher.rb +383 -0
  162. data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
  163. data/test/test_save_state_manager.rb +2 -4
  164. data/test/test_settings_audio.rb +107 -0
  165. data/test/test_settings_hotkeys.rb +83 -66
  166. data/test/test_settings_recording.rb +49 -0
  167. data/test/test_settings_save_states.rb +97 -0
  168. data/test/test_settings_system.rb +133 -0
  169. data/test/test_settings_video.rb +450 -0
  170. data/test/test_settings_window.rb +76 -507
  171. data/test/test_tip_service.rb +6 -6
  172. data/test/test_toast_overlay.rb +1 -1
  173. data/test/test_virtual_events.rb +221 -0
  174. data/test/test_virtual_keyboard.rb +1 -1
  175. data/vendor/rcheevos/CHANGELOG.md +495 -0
  176. data/vendor/rcheevos/LICENSE +21 -0
  177. data/vendor/rcheevos/Package.swift +33 -0
  178. data/vendor/rcheevos/README.md +67 -0
  179. data/vendor/rcheevos/include/module.modulemap +70 -0
  180. data/vendor/rcheevos/include/rc_api_editor.h +296 -0
  181. data/vendor/rcheevos/include/rc_api_info.h +280 -0
  182. data/vendor/rcheevos/include/rc_api_request.h +77 -0
  183. data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
  184. data/vendor/rcheevos/include/rc_api_user.h +262 -0
  185. data/vendor/rcheevos/include/rc_client.h +877 -0
  186. data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
  187. data/vendor/rcheevos/include/rc_consoles.h +138 -0
  188. data/vendor/rcheevos/include/rc_error.h +59 -0
  189. data/vendor/rcheevos/include/rc_export.h +100 -0
  190. data/vendor/rcheevos/include/rc_hash.h +200 -0
  191. data/vendor/rcheevos/include/rc_runtime.h +148 -0
  192. data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
  193. data/vendor/rcheevos/include/rc_util.h +51 -0
  194. data/vendor/rcheevos/include/rcheevos.h +8 -0
  195. data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
  196. data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
  197. data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
  198. data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
  199. data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
  200. data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
  201. data/vendor/rcheevos/src/rc_client.c +6941 -0
  202. data/vendor/rcheevos/src/rc_client_external.c +281 -0
  203. data/vendor/rcheevos/src/rc_client_external.h +177 -0
  204. data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
  205. data/vendor/rcheevos/src/rc_client_internal.h +409 -0
  206. data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
  207. data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
  208. data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
  209. data/vendor/rcheevos/src/rc_compat.c +251 -0
  210. data/vendor/rcheevos/src/rc_compat.h +121 -0
  211. data/vendor/rcheevos/src/rc_libretro.c +915 -0
  212. data/vendor/rcheevos/src/rc_libretro.h +98 -0
  213. data/vendor/rcheevos/src/rc_util.c +199 -0
  214. data/vendor/rcheevos/src/rc_version.c +11 -0
  215. data/vendor/rcheevos/src/rc_version.h +32 -0
  216. data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
  217. data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
  218. data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
  219. data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
  220. data/vendor/rcheevos/src/rcheevos/format.c +330 -0
  221. data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
  222. data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
  223. data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
  224. data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
  225. data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
  226. data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
  227. data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
  228. data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
  229. data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
  230. data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
  231. data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
  232. data/vendor/rcheevos/src/rcheevos/value.c +935 -0
  233. data/vendor/rcheevos/src/rhash/aes.c +480 -0
  234. data/vendor/rcheevos/src/rhash/aes.h +49 -0
  235. data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
  236. data/vendor/rcheevos/src/rhash/hash.c +1402 -0
  237. data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
  238. data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
  239. data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
  240. data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
  241. data/vendor/rcheevos/src/rhash/md5.c +382 -0
  242. data/vendor/rcheevos/src/rhash/md5.h +91 -0
  243. data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
  244. data/vendor/rcheevos/test/libretro.h +205 -0
  245. data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
  246. data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
  247. data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
  248. data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
  249. data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
  250. data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
  251. data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
  252. data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
  253. data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
  254. data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
  255. data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
  256. data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
  257. data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
  258. data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
  259. data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
  260. data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
  261. data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
  262. data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
  263. data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
  264. data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
  265. data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
  266. data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
  267. data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
  268. data/vendor/rcheevos/test/rhash/data.c +657 -0
  269. data/vendor/rcheevos/test/rhash/data.h +32 -0
  270. data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
  271. data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
  272. data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
  273. data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
  274. data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
  275. data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
  276. data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
  277. data/vendor/rcheevos/test/test.c +113 -0
  278. data/vendor/rcheevos/test/test_framework.h +205 -0
  279. data/vendor/rcheevos/test/test_rc_client.c +10509 -0
  280. data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
  281. data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
  282. data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
  283. data/vendor/rcheevos/test/test_types.natvis +9 -0
  284. data/vendor/rcheevos/validator/validator.c +658 -0
  285. data/vendor/rcheevos/validator/validator.vcxproj +152 -0
  286. data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
  287. metadata +277 -10
  288. data/lib/gemba/input_mappings.rb +0 -214
  289. data/lib/gemba/player.rb +0 -1525
@@ -0,0 +1,391 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "shared/tk_test_helper"
5
+
6
+ class TestListPickerFrame < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+
10
+
11
+ # ── Population ─────────────────────────────────────────────────────────────
12
+
13
+ def test_empty_library_shows_no_rows
14
+ assert_tk_app("empty library produces zero treeview rows") do
15
+ require "gemba/headless"
16
+
17
+ lib = Struct.new(:roms) { def all = roms }.new([])
18
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
19
+ picker.show
20
+ app.update
21
+
22
+ items = app.tcl_eval(".list_picker.tree children {}").split
23
+ assert_empty items, "no rows expected for empty library"
24
+
25
+ app.command(:destroy, '.list_picker') rescue nil
26
+ end
27
+ end
28
+
29
+ def test_roms_populate_title_column
30
+ assert_tk_app("ROM titles appear in the title column") do
31
+ require "gemba/headless"
32
+
33
+ roms = [
34
+ { 'title' => 'Alpha', 'platform' => 'gba', 'path' => '/a.gba',
35
+ 'last_played' => '2026-02-20T10:00:00Z' },
36
+ { 'title' => 'Beta', 'platform' => 'gbc', 'path' => '/b.gbc',
37
+ 'last_played' => '2026-02-19T10:00:00Z' },
38
+ ]
39
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
40
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
41
+ picker.show
42
+ app.update
43
+
44
+ items = app.tcl_eval(".list_picker.tree children {}").split
45
+ titles = items.map { |id| app.tcl_eval(".list_picker.tree set #{id} title") }
46
+ assert_equal 2, titles.size
47
+ assert_includes titles, 'Alpha'
48
+ assert_includes titles, 'Beta'
49
+
50
+ app.command(:destroy, '.list_picker') rescue nil
51
+ end
52
+ end
53
+
54
+ def test_last_played_formatted_as_month_day_year
55
+ assert_tk_app("last_played ISO string is formatted for display") do
56
+ require "gemba/headless"
57
+
58
+ roms = [{ 'title' => 'Game', 'platform' => 'gba', 'path' => '/g.gba',
59
+ 'last_played' => '2026-02-22T15:30:00Z' }]
60
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
61
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
62
+ picker.show
63
+ app.update
64
+
65
+ iid = app.tcl_eval(".list_picker.tree children {}").split.first
66
+ lp = app.tcl_eval(".list_picker.tree set #{iid} last_played")
67
+ assert_match(/Feb\s+22,\s+2026/, lp)
68
+
69
+ app.command(:destroy, '.list_picker') rescue nil
70
+ end
71
+ end
72
+
73
+ def test_nil_last_played_shows_never_placeholder
74
+ assert_tk_app("ROM with no last_played shows never-played text, not a date") do
75
+ require "gemba/headless"
76
+
77
+ roms = [{ 'title' => 'New Game', 'platform' => 'gba', 'path' => '/n.gba' }]
78
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
79
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
80
+ picker.show
81
+ app.update
82
+
83
+ iid = app.tcl_eval(".list_picker.tree children {}").split.first
84
+ lp = app.tcl_eval(".list_picker.tree set #{iid} last_played")
85
+ refute_empty lp
86
+ refute_match(/\d{4}/, lp, "should not look like a date")
87
+
88
+ app.command(:destroy, '.list_picker') rescue nil
89
+ end
90
+ end
91
+
92
+ def test_all_roms_shown_without_cap
93
+ assert_tk_app("all 20 library ROMs appear with no cap") do
94
+ require "gemba/headless"
95
+
96
+ roms = 20.times.map { |i| { 'title' => "Game #{i}", 'platform' => 'gba', 'path' => "/g#{i}.gba" } }
97
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
98
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
99
+ picker.show
100
+ app.update
101
+
102
+ count = app.tcl_eval(".list_picker.tree children {}").split.size
103
+ assert_equal 20, count
104
+
105
+ app.command(:destroy, '.list_picker') rescue nil
106
+ end
107
+ end
108
+
109
+ # ── Default sort ────────────────────────────────────────────────────────────
110
+
111
+ def test_default_sort_newest_last_played_first
112
+ assert_tk_app("default sort shows most-recently-played ROM first") do
113
+ require "gemba/headless"
114
+
115
+ roms = [
116
+ { 'title' => 'Old', 'platform' => 'gba', 'path' => '/o.gba',
117
+ 'last_played' => '2024-01-01T00:00:00Z' },
118
+ { 'title' => 'Recent', 'platform' => 'gba', 'path' => '/r.gba',
119
+ 'last_played' => '2026-02-22T00:00:00Z' },
120
+ { 'title' => 'Middle', 'platform' => 'gba', 'path' => '/m.gba',
121
+ 'last_played' => '2025-06-15T00:00:00Z' },
122
+ ]
123
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
124
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
125
+ picker.show
126
+ app.update
127
+
128
+ items = app.tcl_eval(".list_picker.tree children {}").split
129
+ titles = items.map { |id| app.tcl_eval(".list_picker.tree set #{id} title") }
130
+ assert_equal 'Recent', titles[0], "most recent should be first"
131
+ assert_equal 'Middle', titles[1]
132
+ assert_equal 'Old', titles[2]
133
+
134
+ app.command(:destroy, '.list_picker') rescue nil
135
+ end
136
+ end
137
+
138
+ # ── Sorting ─────────────────────────────────────────────────────────────────
139
+
140
+ def test_click_title_heading_sorts_a_to_z_with_indicator
141
+ assert_tk_app("clicking title heading sorts A→Z and shows ▲") do
142
+ require "gemba/headless"
143
+
144
+ roms = [
145
+ { 'title' => 'Zelda', 'platform' => 'gba', 'path' => '/z.gba' },
146
+ { 'title' => 'Metroid', 'platform' => 'gba', 'path' => '/m.gba' },
147
+ { 'title' => 'Castlevania', 'platform' => 'gba', 'path' => '/c.gba' },
148
+ ]
149
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
150
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
151
+ picker.show
152
+ app.update
153
+
154
+ cmd = app.tcl_eval(".list_picker.tree heading title -command")
155
+ app.tcl_eval("uplevel #0 {#{cmd}}")
156
+ app.update
157
+
158
+ items = app.tcl_eval(".list_picker.tree children {}").split
159
+ titles = items.map { |id| app.tcl_eval(".list_picker.tree set #{id} title") }
160
+ assert_equal %w[Castlevania Metroid Zelda], titles
161
+ assert_includes app.tcl_eval(".list_picker.tree heading title -text"), '▲'
162
+
163
+ app.command(:destroy, '.list_picker') rescue nil
164
+ end
165
+ end
166
+
167
+ def test_click_title_heading_twice_reverses_to_z_to_a
168
+ assert_tk_app("clicking title heading twice reverses to Z→A and shows ▼") do
169
+ require "gemba/headless"
170
+
171
+ roms = [
172
+ { 'title' => 'Zelda', 'platform' => 'gba', 'path' => '/z.gba' },
173
+ { 'title' => 'Metroid', 'platform' => 'gba', 'path' => '/m.gba' },
174
+ ]
175
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
176
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
177
+ picker.show
178
+ app.update
179
+
180
+ cmd = app.tcl_eval(".list_picker.tree heading title -command")
181
+ app.tcl_eval("uplevel #0 {#{cmd}}") # asc
182
+ app.tcl_eval("uplevel #0 {#{cmd}}") # desc
183
+ app.update
184
+
185
+ items = app.tcl_eval(".list_picker.tree children {}").split
186
+ titles = items.map { |id| app.tcl_eval(".list_picker.tree set #{id} title") }
187
+ assert_equal %w[Zelda Metroid], titles
188
+ assert_includes app.tcl_eval(".list_picker.tree heading title -text"), '▼'
189
+
190
+ app.command(:destroy, '.list_picker') rescue nil
191
+ end
192
+ end
193
+
194
+ def test_click_last_played_heading_sorts_oldest_first
195
+ assert_tk_app("clicking last_played heading (toggle from default desc) shows oldest first") do
196
+ require "gemba/headless"
197
+
198
+ roms = [
199
+ { 'title' => 'Old', 'platform' => 'gba', 'path' => '/o.gba',
200
+ 'last_played' => '2024-01-01T00:00:00Z' },
201
+ { 'title' => 'Recent', 'platform' => 'gba', 'path' => '/r.gba',
202
+ 'last_played' => '2026-02-22T00:00:00Z' },
203
+ ]
204
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
205
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
206
+ picker.show
207
+ app.update
208
+
209
+ # Default is last_played desc — clicking once switches to asc (oldest first)
210
+ cmd = app.tcl_eval(".list_picker.tree heading last_played -command")
211
+ app.tcl_eval("uplevel #0 {#{cmd}}")
212
+ app.update
213
+
214
+ items = app.tcl_eval(".list_picker.tree children {}").split
215
+ titles = items.map { |id| app.tcl_eval(".list_picker.tree set #{id} title") }
216
+ assert_equal 'Old', titles[0], "ascending: oldest first"
217
+ assert_includes app.tcl_eval(".list_picker.tree heading last_played -text"), '▲'
218
+
219
+ app.command(:destroy, '.list_picker') rescue nil
220
+ end
221
+ end
222
+
223
+ def test_switching_sort_column_clears_old_indicator
224
+ assert_tk_app("switching sort column removes ▲/▼ from the old column") do
225
+ require "gemba/headless"
226
+
227
+ roms = [{ 'title' => 'Solo', 'platform' => 'gba', 'path' => '/s.gba' }]
228
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
229
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
230
+ picker.show
231
+ app.update
232
+
233
+ title_cmd = app.tcl_eval(".list_picker.tree heading title -command")
234
+ lp_cmd = app.tcl_eval(".list_picker.tree heading last_played -command")
235
+
236
+ app.tcl_eval("uplevel #0 {#{title_cmd}}") # sort by title (adds ▲)
237
+ app.tcl_eval("uplevel #0 {#{lp_cmd}}") # switch to last_played
238
+ app.update
239
+
240
+ title_text = app.tcl_eval(".list_picker.tree heading title -text")
241
+ refute_includes title_text, '▲', "title heading should lose its indicator"
242
+ refute_includes title_text, '▼', "title heading should lose its indicator"
243
+
244
+ lp_text = app.tcl_eval(".list_picker.tree heading last_played -text")
245
+ assert(lp_text.include?('▲') || lp_text.include?('▼'),
246
+ "last_played heading should now show an indicator")
247
+
248
+ app.command(:destroy, '.list_picker') rescue nil
249
+ end
250
+ end
251
+
252
+ # ── Interaction ─────────────────────────────────────────────────────────────
253
+
254
+ def test_double_click_row_emits_rom_selected
255
+ assert_tk_app("double-clicking a row emits :rom_selected with the ROM path") do
256
+ require "gemba/headless"
257
+
258
+ rom_path = '/games/fire_red.gba'
259
+ roms = [{ 'title' => 'Fire Red', 'platform' => 'gba', 'path' => rom_path }]
260
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
261
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
262
+
263
+ received = nil
264
+ Gemba.bus.on(:rom_selected) { |path| received = path }
265
+
266
+ picker.show
267
+ app.show
268
+ app.update
269
+
270
+ iid = app.tcl_eval(".list_picker.tree children {}").split.first
271
+ app.tcl_eval(".list_picker.tree focus #{iid}")
272
+ app.tcl_eval(".list_picker.tree selection set #{iid}")
273
+ app.tcl_eval("event generate .list_picker.tree <<DoubleClick>>")
274
+ app.update
275
+
276
+ assert_equal rom_path, received
277
+
278
+ app.command(:destroy, '.list_picker') rescue nil
279
+ end
280
+ end
281
+
282
+ def test_right_click_quick_load_disabled_when_no_save_state
283
+ assert_tk_app("<<RightClick>> quick load entry is disabled with no save file") do
284
+ require "gemba/headless"
285
+ require "tmpdir"
286
+
287
+ Dir.mktmpdir("list_picker_qs_test") do |tmpdir|
288
+ rom_id = "AGB-TEST-DEADBEEF"
289
+ roms = [{ 'title' => 'Test', 'platform' => 'gba',
290
+ 'rom_id' => rom_id, 'game_code' => 'AGB-TEST',
291
+ 'path' => '/games/test.gba', 'last_played' => '2026-01-01T00:00:00Z' }]
292
+
293
+ Gemba.user_config.states_dir = tmpdir
294
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
295
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
296
+ picker.show
297
+ app.show
298
+ app.update
299
+
300
+ iid = app.tcl_eval(".list_picker.tree children {}").split.first
301
+ app.tcl_eval(".list_picker.tree focus #{iid}")
302
+ app.tcl_eval(".list_picker.tree selection set #{iid}")
303
+
304
+ override_tk_popup do
305
+ app.tcl_eval("event generate .list_picker.tree <<RightClick>>")
306
+ app.update
307
+ end
308
+
309
+ state = app.tcl_eval(".list_picker.tree.ctx entrycget 1 -state")
310
+ assert_equal 'disabled', state
311
+
312
+ app.command(:destroy, '.list_picker') rescue nil
313
+ end
314
+ end
315
+ end
316
+
317
+ def test_right_click_quick_load_enabled_when_save_state_exists
318
+ assert_tk_app("<<RightClick>> quick load entry is enabled when save file exists") do
319
+ require "gemba/headless"
320
+ require "tmpdir"
321
+ require "fileutils"
322
+
323
+ fixture = File.expand_path("test/fixtures/test_quicksave.ss")
324
+
325
+ Dir.mktmpdir("list_picker_qs_test") do |tmpdir|
326
+ rom_id = "AGB-TEST-DEADBEEF"
327
+ slot = Gemba.user_config.quick_save_slot
328
+ state_dir = File.join(tmpdir, rom_id)
329
+ FileUtils.mkdir_p(state_dir)
330
+ FileUtils.cp(fixture, File.join(state_dir, "state#{slot}.ss"))
331
+
332
+ roms = [{ 'title' => 'Test', 'platform' => 'gba',
333
+ 'rom_id' => rom_id, 'game_code' => 'AGB-TEST',
334
+ 'path' => '/games/test.gba', 'last_played' => '2026-01-01T00:00:00Z' }]
335
+
336
+ Gemba.user_config.states_dir = tmpdir
337
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
338
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
339
+ picker.show
340
+ app.show
341
+ app.update
342
+
343
+ iid = app.tcl_eval(".list_picker.tree children {}").split.first
344
+ app.tcl_eval(".list_picker.tree focus #{iid}")
345
+ app.tcl_eval(".list_picker.tree selection set #{iid}")
346
+
347
+ override_tk_popup do
348
+ app.tcl_eval("event generate .list_picker.tree <<RightClick>>")
349
+ app.update
350
+ end
351
+
352
+ state = app.tcl_eval(".list_picker.tree.ctx entrycget 1 -state")
353
+ assert_equal 'normal', state
354
+
355
+ app.command(:destroy, '.list_picker') rescue nil
356
+ end
357
+ end
358
+ end
359
+
360
+ def test_refresh_repopulates_rows
361
+ assert_tk_app("receive(:refresh) updates rows from the current library state") do
362
+ require "gemba/headless"
363
+
364
+ roms = [{ 'title' => 'Alpha', 'platform' => 'gba', 'path' => '/a.gba' }]
365
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
366
+
367
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
368
+ picker.show
369
+ app.update
370
+
371
+ items = app.tcl_eval(".list_picker.tree children {}").split
372
+ titles = items.map { |id| app.tcl_eval(".list_picker.tree set #{id} title") }
373
+ assert_equal ['Alpha'], titles
374
+
375
+ lib.roms = [
376
+ { 'title' => 'Alpha', 'platform' => 'gba', 'path' => '/a.gba' },
377
+ { 'title' => 'Beta', 'platform' => 'gbc', 'path' => '/b.gbc' },
378
+ ]
379
+ picker.receive(:refresh)
380
+ app.update
381
+
382
+ items = app.tcl_eval(".list_picker.tree children {}").split
383
+ titles = items.map { |id| app.tcl_eval(".list_picker.tree set #{id} title") }
384
+ assert_equal 2, titles.size
385
+ assert_includes titles, 'Alpha'
386
+ assert_includes titles, 'Beta'
387
+
388
+ app.command(:destroy, '.list_picker') rescue nil
389
+ end
390
+ end
391
+ end
data/test/test_locale.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "minitest/autorun"
4
4
  require "yaml"
5
- require_relative "../lib/gemba/locale"
5
+ require "gemba/headless"
6
6
 
7
7
  class TestMGBALocale < Minitest::Test
8
8
  # -- Loading ---------------------------------------------------------------
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "tmpdir"
5
+ require "fileutils"
6
+ require "gemba/headless"
7
+
8
+ class TestLogging < Minitest::Test
9
+ def setup
10
+ @dir = Dir.mktmpdir("gemba-logs-test")
11
+ @logger = Gemba::SessionLogger.new(dir: @dir, level: :info)
12
+ end
13
+
14
+ def teardown
15
+ Gemba.logger = nil
16
+ FileUtils.rm_rf(@dir)
17
+ end
18
+
19
+ # -- lazy file creation --
20
+
21
+ def test_no_file_before_first_log
22
+ assert_empty Dir.glob(File.join(@dir, "*.log"))
23
+ end
24
+
25
+ def test_file_created_on_first_log
26
+ @logger.log(:info) { "hello" }
27
+ logs = Dir.glob(File.join(@dir, "*.log"))
28
+ assert_equal 1, logs.length
29
+ end
30
+
31
+ def test_file_named_by_date
32
+ @logger.log(:info) { "hello" }
33
+ logs = Dir.glob(File.join(@dir, "*.log"))
34
+ assert_match(/gemba-\d{4}-\d{2}-\d{2}\.log/, File.basename(logs.first))
35
+ end
36
+
37
+ # -- level filtering --
38
+
39
+ def test_filters_below_level
40
+ @logger.log(:debug) { "should not appear" }
41
+ assert_empty Dir.glob(File.join(@dir, "*.log")),
42
+ "Debug message should not create log file at info level"
43
+ end
44
+
45
+ def test_allows_at_level
46
+ @logger.log(:info) { "visible" }
47
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
48
+ assert_includes content, "visible"
49
+ end
50
+
51
+ def test_allows_above_level
52
+ @logger.log(:error) { "bad thing" }
53
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
54
+ assert_includes content, "bad thing"
55
+ end
56
+
57
+ def test_debug_level_allows_debug
58
+ logger = Gemba::SessionLogger.new(dir: @dir, level: :debug)
59
+ logger.log(:debug) { "debug msg" }
60
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
61
+ assert_includes content, "debug msg"
62
+ end
63
+
64
+ # -- log format --
65
+
66
+ def test_log_format
67
+ @logger.log(:info) { "test message" }
68
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
69
+ assert_match(/\d{2}:\d{2}:\d{2}\.\d{3} \[INFO\] test message/, content)
70
+ end
71
+
72
+ # -- auto-prune --
73
+
74
+ def test_prune_keeps_max_files
75
+ # Create 30 fake log files
76
+ 30.times do |i|
77
+ File.write(File.join(@dir, "gemba-2026-01-#{format('%02d', i + 1)}.log"), "old")
78
+ end
79
+
80
+ # New logger prunes on init
81
+ Gemba::SessionLogger.new(dir: @dir, level: :info)
82
+
83
+ remaining = Dir.glob(File.join(@dir, "gemba-*.log"))
84
+ assert_equal Gemba::SessionLogger::MAX_LOG_FILES, remaining.length
85
+ end
86
+
87
+ def test_prune_keeps_newest
88
+ 30.times do |i|
89
+ File.write(File.join(@dir, "gemba-2026-01-#{format('%02d', i + 1)}.log"), "old")
90
+ end
91
+
92
+ Gemba::SessionLogger.new(dir: @dir, level: :info)
93
+
94
+ remaining = Dir.glob(File.join(@dir, "gemba-*.log")).sort
95
+ # Should keep the last 25 (days 06-30)
96
+ assert_equal "gemba-2026-01-06.log", File.basename(remaining.first)
97
+ assert_equal "gemba-2026-01-30.log", File.basename(remaining.last)
98
+ end
99
+
100
+ def test_prune_no_op_when_under_limit
101
+ 3.times do |i|
102
+ File.write(File.join(@dir, "gemba-2026-01-#{format('%02d', i + 1)}.log"), "ok")
103
+ end
104
+
105
+ Gemba::SessionLogger.new(dir: @dir, level: :info)
106
+ assert_equal 3, Dir.glob(File.join(@dir, "gemba-*.log")).length
107
+ end
108
+
109
+ # -- module interface --
110
+
111
+ def test_gemba_log_module_method
112
+ Gemba.logger = Gemba::SessionLogger.new(dir: @dir, level: :info)
113
+ Gemba.log(:info) { "module test" }
114
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
115
+ assert_includes content, "module test"
116
+ end
117
+
118
+ def test_gemba_logger_setter
119
+ custom = Gemba::SessionLogger.new(dir: @dir, level: :warn)
120
+ Gemba.logger = custom
121
+ assert_same custom, Gemba.logger
122
+ end
123
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/achievements"
6
+ require_relative "support/fake_core"
7
+
8
+ class TestNullBackend < Minitest::Test
9
+ def setup
10
+ @b = Gemba::Achievements::NullBackend.new
11
+ end
12
+
13
+ def test_not_enabled
14
+ refute @b.enabled?
15
+ end
16
+
17
+ def test_not_authenticated
18
+ refute @b.authenticated?
19
+ end
20
+
21
+ def test_achievement_list_empty
22
+ assert_equal [], @b.achievement_list
23
+ end
24
+
25
+ def test_counts_zero
26
+ assert_equal 0, @b.earned_count
27
+ assert_equal 0, @b.total_count
28
+ end
29
+
30
+ def test_do_frame_is_noop
31
+ assert_nil @b.do_frame(FakeCore.new)
32
+ end
33
+
34
+ def test_login_noop
35
+ assert_nil @b.login_with_token(username: 'u', token: 't')
36
+ refute @b.authenticated?
37
+ end
38
+
39
+ def test_logout_noop
40
+ assert_nil @b.logout
41
+ end
42
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+
6
+ class TestOfflineBackend < Minitest::Test
7
+ ROM = "test/fixtures/test.gba"
8
+
9
+ def setup
10
+ skip "test.gba fixture not found" unless File.exist?(ROM)
11
+ @unlocked = []
12
+ @backend = Gemba::Achievements::OfflineBackend.new
13
+ @backend.on_unlock { |ach| @unlocked << ach }
14
+ end
15
+
16
+ def test_always_authenticated
17
+ assert @backend.authenticated?
18
+ end
19
+
20
+ def test_enabled
21
+ assert @backend.enabled?
22
+ end
23
+
24
+ def test_login_and_logout_are_noops
25
+ @backend.login_with_password(username: "anyone", password: "anything")
26
+ assert @backend.authenticated?
27
+ @backend.logout
28
+ assert @backend.authenticated?
29
+ end
30
+
31
+ def test_on_load_achievement_fires_during_load_game
32
+ Gemba::HeadlessPlayer.open(ROM) do |player|
33
+ @backend.load_game(player.core)
34
+ assert_equal 1, @unlocked.size
35
+ assert_equal "gembatest_loaded", @unlocked.first.id
36
+ assert_equal "Ready to Play", @unlocked.first.title
37
+ assert @unlocked.first.earned?
38
+ end
39
+ end
40
+
41
+ def test_achievement_list_shows_earned_after_load
42
+ Gemba::HeadlessPlayer.open(ROM) do |player|
43
+ @backend.load_game(player.core)
44
+ list = @backend.achievement_list
45
+ assert_equal 1, list.size
46
+ assert list.first.earned?
47
+ end
48
+ end
49
+
50
+ def test_counts
51
+ Gemba::HeadlessPlayer.open(ROM) do |player|
52
+ assert_equal 0, @backend.total_count
53
+ @backend.load_game(player.core)
54
+ assert_equal 1, @backend.total_count
55
+ assert_equal 1, @backend.earned_count
56
+ end
57
+ end
58
+
59
+ def test_unload_game_clears_state
60
+ Gemba::HeadlessPlayer.open(ROM) do |player|
61
+ @backend.load_game(player.core)
62
+ assert_equal 1, @backend.earned_count
63
+ @backend.unload_game
64
+ assert_equal 0, @backend.total_count
65
+ assert_equal 0, @backend.earned_count
66
+ end
67
+ end
68
+
69
+ def test_unknown_rom_has_no_achievements
70
+ Gemba::HeadlessPlayer.open(ROM) do |player|
71
+ custom = Gemba::Achievements::OfflineBackend.new(db: {})
72
+ custom.load_game(player.core)
73
+ assert_equal 0, custom.total_count
74
+ assert_empty @unlocked
75
+ end
76
+ end
77
+
78
+ def test_store_adds_definitions
79
+ Gemba::HeadlessPlayer.open(ROM) do |player|
80
+ custom = Gemba::Achievements::OfflineBackend.new(db: {})
81
+ custom.on_unlock { |a| @unlocked << a }
82
+ custom.store(player.core.checksum, [
83
+ { id: "extra", title: "Extra", description: "desc",
84
+ points: 5, trigger: :on_load }
85
+ ])
86
+ custom.load_game(player.core)
87
+ assert_equal 1, @unlocked.size
88
+ assert_equal "extra", @unlocked.first.id
89
+ end
90
+ end
91
+
92
+ def test_memory_achievement_fires_on_rising_edge
93
+ addr = 0x02000000
94
+ Gemba::HeadlessPlayer.open(ROM) do |player|
95
+ backend = Gemba::Achievements::OfflineBackend.new(db: {
96
+ player.core.checksum => [
97
+ { id: "mem_test", title: "Mem", description: "d", points: 2,
98
+ trigger: :memory,
99
+ condition: ->(mem) { mem.call(addr) == 0x01 } }
100
+ ]
101
+ })
102
+ backend.on_unlock { |a| @unlocked << a }
103
+ backend.load_game(player.core)
104
+
105
+ # EWRAM starts zeroed — condition false
106
+ player.step(1)
107
+ backend.do_frame(player.core)
108
+ assert_empty @unlocked
109
+
110
+ # Write 0x01 to EWRAM — but we can't poke real memory from Ruby,
111
+ # so verify do_frame doesn't crash and condition stays unevaluated
112
+ backend.do_frame(player.core)
113
+ assert_empty @unlocked
114
+ end
115
+ end
116
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "minitest/autorun"
4
- require_relative "../lib/gemba/overlay_renderer"
4
+ require "gemba/headless"
5
5
 
6
6
  class TestOverlayRenderer < Minitest::Test
7
7
  class MockTexture