gemba 0.1.1 → 0.2.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.
Files changed (285) 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 +186 -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 +453 -0
  17. data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
  18. data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
  19. data/lib/gemba/achievements.rb +19 -0
  20. data/lib/gemba/achievements_window.rb +556 -0
  21. data/lib/gemba/app_controller.rb +1015 -0
  22. data/lib/gemba/bios.rb +54 -0
  23. data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
  24. data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
  25. data/lib/gemba/boxart_fetcher.rb +79 -0
  26. data/lib/gemba/bus_emitter.rb +13 -0
  27. data/lib/gemba/child_window.rb +24 -1
  28. data/lib/gemba/cli/commands/config_cmd.rb +83 -0
  29. data/lib/gemba/cli/commands/decode.rb +154 -0
  30. data/lib/gemba/cli/commands/patch.rb +78 -0
  31. data/lib/gemba/cli/commands/play.rb +78 -0
  32. data/lib/gemba/cli/commands/record.rb +114 -0
  33. data/lib/gemba/cli/commands/replay.rb +161 -0
  34. data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
  35. data/lib/gemba/cli/commands/version.rb +22 -0
  36. data/lib/gemba/cli.rb +52 -364
  37. data/lib/gemba/config.rb +134 -1
  38. data/lib/gemba/data/gb_games.json +1 -0
  39. data/lib/gemba/data/gb_md5.json +1 -0
  40. data/lib/gemba/data/gba_games.json +1 -0
  41. data/lib/gemba/data/gba_md5.json +1 -0
  42. data/lib/gemba/data/gbc_games.json +1 -0
  43. data/lib/gemba/data/gbc_md5.json +1 -0
  44. data/lib/gemba/emulator_frame.rb +1060 -0
  45. data/lib/gemba/event_bus.rb +48 -0
  46. data/lib/gemba/frame_stack.rb +60 -0
  47. data/lib/gemba/game_index.rb +84 -0
  48. data/lib/gemba/game_picker_frame.rb +268 -0
  49. data/lib/gemba/gamepad_map.rb +103 -0
  50. data/lib/gemba/headless.rb +6 -5
  51. data/lib/gemba/headless_player.rb +33 -3
  52. data/lib/gemba/help_window.rb +61 -0
  53. data/lib/gemba/hotkey_map.rb +3 -1
  54. data/lib/gemba/input_recorder.rb +107 -0
  55. data/lib/gemba/input_replayer.rb +119 -0
  56. data/lib/gemba/keyboard_map.rb +90 -0
  57. data/lib/gemba/locales/en.yml +97 -5
  58. data/lib/gemba/locales/ja.yml +97 -5
  59. data/lib/gemba/main_window.rb +56 -0
  60. data/lib/gemba/modal_stack.rb +81 -0
  61. data/lib/gemba/patcher_window.rb +223 -0
  62. data/lib/gemba/platform/gb.rb +21 -0
  63. data/lib/gemba/platform/gba.rb +21 -0
  64. data/lib/gemba/platform/gbc.rb +23 -0
  65. data/lib/gemba/platform.rb +20 -0
  66. data/lib/gemba/platform_open.rb +19 -0
  67. data/lib/gemba/recorder.rb +4 -3
  68. data/lib/gemba/replay_player.rb +691 -0
  69. data/lib/gemba/rom_info.rb +57 -0
  70. data/lib/gemba/rom_info_window.rb +16 -3
  71. data/lib/gemba/rom_library.rb +106 -0
  72. data/lib/gemba/rom_overrides.rb +47 -0
  73. data/lib/gemba/rom_patcher/bps.rb +161 -0
  74. data/lib/gemba/rom_patcher/ips.rb +101 -0
  75. data/lib/gemba/rom_patcher/ups.rb +118 -0
  76. data/lib/gemba/rom_patcher.rb +109 -0
  77. data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
  78. data/lib/gemba/runtime.rb +59 -26
  79. data/lib/gemba/save_state_manager.rb +4 -7
  80. data/lib/gemba/save_state_picker.rb +17 -4
  81. data/lib/gemba/session_logger.rb +64 -0
  82. data/lib/gemba/settings/audio_tab.rb +77 -0
  83. data/lib/gemba/settings/gamepad_tab.rb +351 -0
  84. data/lib/gemba/settings/hotkeys_tab.rb +259 -0
  85. data/lib/gemba/settings/paths.rb +11 -0
  86. data/lib/gemba/settings/recording_tab.rb +83 -0
  87. data/lib/gemba/settings/save_states_tab.rb +91 -0
  88. data/lib/gemba/settings/system_tab.rb +362 -0
  89. data/lib/gemba/settings/video_tab.rb +318 -0
  90. data/lib/gemba/settings_window.rb +162 -1036
  91. data/lib/gemba/version.rb +1 -1
  92. data/lib/gemba/virtual_keyboard.rb +19 -0
  93. data/lib/gemba.rb +2 -12
  94. data/test/achievements_window/test_bulk_sync.rb +218 -0
  95. data/test/achievements_window/test_bus_events.rb +125 -0
  96. data/test/achievements_window/test_close_confirmation.rb +201 -0
  97. data/test/achievements_window/test_initial_state.rb +164 -0
  98. data/test/achievements_window/test_sorting.rb +227 -0
  99. data/test/achievements_window/test_tree_rendering.rb +133 -0
  100. data/test/fixtures/fake_bios.bin +0 -0
  101. data/test/fixtures/pong.gba +0 -0
  102. data/test/fixtures/test.gb +0 -0
  103. data/test/fixtures/test.gbc +0 -0
  104. data/test/fixtures/test_quicksave.ss +0 -0
  105. data/test/screenshots/no_focus.png +0 -0
  106. data/test/shared/teek_test_worker.rb +17 -1
  107. data/test/shared/tk_test_helper.rb +91 -4
  108. data/test/support/achievements_window_helpers.rb +18 -0
  109. data/test/support/fake_core.rb +25 -0
  110. data/test/support/fake_ra_runtime.rb +74 -0
  111. data/test/support/fake_requester.rb +68 -0
  112. data/test/support/player_helpers.rb +20 -5
  113. data/test/test_achievement.rb +32 -0
  114. data/test/{test_player.rb → test_app_controller.rb} +353 -85
  115. data/test/test_bios.rb +123 -0
  116. data/test/test_boxart_fetcher.rb +150 -0
  117. data/test/test_cli.rb +17 -265
  118. data/test/test_cli_config.rb +64 -0
  119. data/test/test_cli_decode.rb +97 -0
  120. data/test/test_cli_patch.rb +58 -0
  121. data/test/test_cli_play.rb +213 -0
  122. data/test/test_cli_ra.rb +175 -0
  123. data/test/test_cli_record.rb +69 -0
  124. data/test/test_cli_replay.rb +72 -0
  125. data/test/test_cli_sync_requester.rb +152 -0
  126. data/test/test_cli_version.rb +27 -0
  127. data/test/test_config.rb +2 -3
  128. data/test/test_config_ra.rb +69 -0
  129. data/test/test_core.rb +62 -1
  130. data/test/test_credentials_presenter.rb +192 -0
  131. data/test/test_event_bus.rb +100 -0
  132. data/test/test_fake_backend_achievements.rb +130 -0
  133. data/test/test_fake_backend_auth.rb +68 -0
  134. data/test/test_game_index.rb +77 -0
  135. data/test/test_game_picker_frame.rb +310 -0
  136. data/test/test_gamepad_map.rb +1 -3
  137. data/test/test_headless_player.rb +17 -3
  138. data/test/test_help_window.rb +82 -0
  139. data/test/test_hotkey_map.rb +22 -1
  140. data/test/test_input_recorder.rb +179 -0
  141. data/test/test_input_replay_determinism.rb +113 -0
  142. data/test/test_input_replayer.rb +162 -0
  143. data/test/test_keyboard_map.rb +1 -3
  144. data/test/test_libretro_backend.rb +41 -0
  145. data/test/test_locale.rb +1 -1
  146. data/test/test_logging.rb +123 -0
  147. data/test/test_null_backend.rb +42 -0
  148. data/test/test_offline_backend.rb +116 -0
  149. data/test/test_overlay_renderer.rb +1 -1
  150. data/test/test_platform.rb +149 -0
  151. data/test/test_ra_backend.rb +313 -0
  152. data/test/test_ra_backend_unlock_gate.rb +56 -0
  153. data/test/test_recorder.rb +0 -3
  154. data/test/test_replay_player.rb +316 -0
  155. data/test/test_rom_info.rb +149 -0
  156. data/test/test_rom_overrides.rb +86 -0
  157. data/test/test_rom_patcher.rb +382 -0
  158. data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
  159. data/test/test_save_state_manager.rb +2 -4
  160. data/test/test_settings_audio.rb +107 -0
  161. data/test/test_settings_hotkeys.rb +83 -66
  162. data/test/test_settings_recording.rb +49 -0
  163. data/test/test_settings_save_states.rb +97 -0
  164. data/test/test_settings_system.rb +133 -0
  165. data/test/test_settings_video.rb +450 -0
  166. data/test/test_settings_window.rb +76 -507
  167. data/test/test_tip_service.rb +6 -6
  168. data/test/test_toast_overlay.rb +1 -1
  169. data/test/test_virtual_events.rb +156 -0
  170. data/test/test_virtual_keyboard.rb +1 -1
  171. data/vendor/rcheevos/CHANGELOG.md +495 -0
  172. data/vendor/rcheevos/LICENSE +21 -0
  173. data/vendor/rcheevos/Package.swift +33 -0
  174. data/vendor/rcheevos/README.md +67 -0
  175. data/vendor/rcheevos/include/module.modulemap +70 -0
  176. data/vendor/rcheevos/include/rc_api_editor.h +296 -0
  177. data/vendor/rcheevos/include/rc_api_info.h +280 -0
  178. data/vendor/rcheevos/include/rc_api_request.h +77 -0
  179. data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
  180. data/vendor/rcheevos/include/rc_api_user.h +262 -0
  181. data/vendor/rcheevos/include/rc_client.h +877 -0
  182. data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
  183. data/vendor/rcheevos/include/rc_consoles.h +138 -0
  184. data/vendor/rcheevos/include/rc_error.h +59 -0
  185. data/vendor/rcheevos/include/rc_export.h +100 -0
  186. data/vendor/rcheevos/include/rc_hash.h +200 -0
  187. data/vendor/rcheevos/include/rc_runtime.h +148 -0
  188. data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
  189. data/vendor/rcheevos/include/rc_util.h +51 -0
  190. data/vendor/rcheevos/include/rcheevos.h +8 -0
  191. data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
  192. data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
  193. data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
  194. data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
  195. data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
  196. data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
  197. data/vendor/rcheevos/src/rc_client.c +6941 -0
  198. data/vendor/rcheevos/src/rc_client_external.c +281 -0
  199. data/vendor/rcheevos/src/rc_client_external.h +177 -0
  200. data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
  201. data/vendor/rcheevos/src/rc_client_internal.h +409 -0
  202. data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
  203. data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
  204. data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
  205. data/vendor/rcheevos/src/rc_compat.c +251 -0
  206. data/vendor/rcheevos/src/rc_compat.h +121 -0
  207. data/vendor/rcheevos/src/rc_libretro.c +915 -0
  208. data/vendor/rcheevos/src/rc_libretro.h +98 -0
  209. data/vendor/rcheevos/src/rc_util.c +199 -0
  210. data/vendor/rcheevos/src/rc_version.c +11 -0
  211. data/vendor/rcheevos/src/rc_version.h +32 -0
  212. data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
  213. data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
  214. data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
  215. data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
  216. data/vendor/rcheevos/src/rcheevos/format.c +330 -0
  217. data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
  218. data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
  219. data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
  220. data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
  221. data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
  222. data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
  223. data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
  224. data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
  225. data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
  226. data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
  227. data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
  228. data/vendor/rcheevos/src/rcheevos/value.c +935 -0
  229. data/vendor/rcheevos/src/rhash/aes.c +480 -0
  230. data/vendor/rcheevos/src/rhash/aes.h +49 -0
  231. data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
  232. data/vendor/rcheevos/src/rhash/hash.c +1402 -0
  233. data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
  234. data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
  235. data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
  236. data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
  237. data/vendor/rcheevos/src/rhash/md5.c +382 -0
  238. data/vendor/rcheevos/src/rhash/md5.h +91 -0
  239. data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
  240. data/vendor/rcheevos/test/libretro.h +205 -0
  241. data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
  242. data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
  243. data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
  244. data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
  245. data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
  246. data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
  247. data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
  248. data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
  249. data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
  250. data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
  251. data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
  252. data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
  253. data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
  254. data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
  255. data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
  256. data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
  257. data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
  258. data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
  259. data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
  260. data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
  261. data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
  262. data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
  263. data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
  264. data/vendor/rcheevos/test/rhash/data.c +657 -0
  265. data/vendor/rcheevos/test/rhash/data.h +32 -0
  266. data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
  267. data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
  268. data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
  269. data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
  270. data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
  271. data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
  272. data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
  273. data/vendor/rcheevos/test/test.c +113 -0
  274. data/vendor/rcheevos/test/test_framework.h +205 -0
  275. data/vendor/rcheevos/test/test_rc_client.c +10509 -0
  276. data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
  277. data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
  278. data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
  279. data/vendor/rcheevos/test/test_types.natvis +9 -0
  280. data/vendor/rcheevos/validator/validator.c +658 -0
  281. data/vendor/rcheevos/validator/validator.vcxproj +152 -0
  282. data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
  283. metadata +274 -11
  284. data/lib/gemba/input_mappings.rb +0 -214
  285. data/lib/gemba/player.rb +0 -1525
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "../shared/tk_test_helper"
5
+
6
+ # Tests for AchievementsWindow initial widget state — sync button, unofficial
7
+ # checkbox, game combo filter, and status bar default text.
8
+ class TestAchievementsWindowInitialState < Minitest::Test
9
+ include TeekTestHelper
10
+
11
+ TOP = Gemba::AchievementsWindow::TOP rescue '.gemba_achievements'
12
+
13
+ def test_sync_button_disabled_when_not_authenticated
14
+ assert_tk_app("sync button disabled when not authenticated") do
15
+ begin
16
+ require "gemba/headless"
17
+ require "support/achievements_window_helpers"
18
+ Gemba.bus = Gemba::EventBus.new
19
+
20
+ backend = Gemba::Achievements::FakeBackend.new # NOT logged in
21
+ win = Gemba::AchievementsWindow.new(
22
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
23
+ )
24
+ win.update_game(rom_id: nil, backend: backend)
25
+ win.show
26
+ app.update
27
+
28
+ top = Gemba::AchievementsWindow::TOP
29
+ assert_equal 'disabled',
30
+ app.command("#{top}.toolbar.sync", :cget, '-state').to_s
31
+ ensure
32
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
33
+ end
34
+ end
35
+ end
36
+
37
+ def test_sync_button_enabled_when_authenticated
38
+ assert_tk_app("sync button enabled when authenticated") do
39
+ begin
40
+ require "gemba/headless"
41
+ require "support/achievements_window_helpers"
42
+ Gemba.bus = Gemba::EventBus.new
43
+
44
+ backend = Gemba::Achievements::FakeBackend.new
45
+ backend.login_with_token(username: 'u', token: 't')
46
+ win = Gemba::AchievementsWindow.new(
47
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
48
+ )
49
+ win.update_game(rom_id: nil, backend: backend)
50
+ win.show
51
+ app.update
52
+
53
+ top = Gemba::AchievementsWindow::TOP
54
+ assert_equal 'normal',
55
+ app.command("#{top}.toolbar.sync", :cget, '-state').to_s
56
+ ensure
57
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
58
+ end
59
+ end
60
+ end
61
+
62
+ def test_unofficial_checkbox_reflects_config_false
63
+ assert_tk_app("unofficial checkbox is unchecked when config is false") do
64
+ begin
65
+ require "gemba/headless"
66
+ require "support/achievements_window_helpers"
67
+ Gemba.bus = Gemba::EventBus.new
68
+
69
+ backend = Gemba::Achievements::FakeBackend.new
70
+ backend.login_with_token(username: 'u', token: 't')
71
+ win = Gemba::AchievementsWindow.new(
72
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
73
+ )
74
+ win.update_game(rom_id: nil, backend: backend)
75
+ win.show
76
+ app.update
77
+
78
+ assert_equal '0', app.get_variable(Gemba::AchievementsWindow::VAR_UNOFFICIAL)
79
+ ensure
80
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
81
+ end
82
+ end
83
+ end
84
+
85
+ def test_unofficial_checkbox_reflects_config_true
86
+ assert_tk_app("unofficial checkbox is checked when config is true") do
87
+ begin
88
+ require "gemba/headless"
89
+ require "support/achievements_window_helpers"
90
+ Gemba.bus = Gemba::EventBus.new
91
+
92
+ backend = Gemba::Achievements::FakeBackend.new
93
+ backend.login_with_token(username: 'u', token: 't')
94
+ win = Gemba::AchievementsWindow.new(
95
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(true)
96
+ )
97
+ win.update_game(rom_id: nil, backend: backend)
98
+ win.show
99
+ app.update
100
+
101
+ assert_equal '1', app.get_variable(Gemba::AchievementsWindow::VAR_UNOFFICIAL)
102
+ ensure
103
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
104
+ end
105
+ end
106
+ end
107
+
108
+ def test_combo_shows_only_gba_games
109
+ assert_tk_app("combo lists GBA games only, not GB or GBC") do
110
+ begin
111
+ require "gemba/headless"
112
+ require "support/achievements_window_helpers"
113
+ Gemba.bus = Gemba::EventBus.new
114
+
115
+ roms = [
116
+ make_rom_entry(id: 'gba1', title: 'GBA Game', platform: 'gba'),
117
+ make_rom_entry(id: 'gb1', title: 'GB Game', platform: 'gb'),
118
+ make_rom_entry(id: 'gbc1', title: 'GBC Game', platform: 'gbc'),
119
+ ]
120
+ backend = Gemba::Achievements::FakeBackend.new
121
+ backend.login_with_token(username: 'u', token: 't')
122
+ win = Gemba::AchievementsWindow.new(
123
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
124
+ )
125
+ win.update_game(rom_id: nil, backend: backend)
126
+ win.show
127
+ app.update
128
+
129
+ top = Gemba::AchievementsWindow::TOP
130
+ values = app.command("#{top}.toolbar.combo", :cget, '-values').to_s
131
+ assert_includes values, 'GBA Game'
132
+ refute_includes values, 'GB Game'
133
+ refute_includes values, 'GBC Game'
134
+ ensure
135
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
136
+ end
137
+ end
138
+ end
139
+
140
+ def test_status_shows_none_when_empty
141
+ assert_tk_app("status shows 'no achievements' when backend has no achievements") do
142
+ begin
143
+ require "gemba/headless"
144
+ require "support/achievements_window_helpers"
145
+ Gemba.bus = Gemba::EventBus.new
146
+
147
+ backend = Gemba::Achievements::FakeBackend.new
148
+ backend.login_with_token(username: 'u', token: 't')
149
+ win = Gemba::AchievementsWindow.new(
150
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
151
+ )
152
+ win.update_game(rom_id: nil, backend: backend)
153
+ win.show
154
+ app.update
155
+
156
+ top = Gemba::AchievementsWindow::TOP
157
+ status = app.command("#{top}.status_bar.status", :cget, '-text').to_s
158
+ assert_equal Gemba::Locale.translate('achievements.none'), status
159
+ ensure
160
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "../shared/tk_test_helper"
5
+
6
+ # Tests for AchievementsWindow treeview column sorting — default order,
7
+ # heading click indicators, and indicator cleanup when switching columns.
8
+ class TestAchievementsWindowSorting < Minitest::Test
9
+ include TeekTestHelper
10
+
11
+ def test_default_sort_earned_first_then_alphabetical
12
+ assert_tk_app("default sort: earned first, then unearned A→Z") do
13
+ begin
14
+ require "gemba/headless"
15
+ require "support/achievements_window_helpers"
16
+ require "support/fake_core"
17
+ Gemba.bus = Gemba::EventBus.new
18
+
19
+ backend = Gemba::Achievements::FakeBackend.new
20
+ backend.login_with_token(username: 'u', token: 't')
21
+ backend.add_achievement(id: 'z', title: 'Zebra', description: '', points: 5) { false }
22
+ backend.add_achievement(id: 'a', title: 'Aardvark', description: '', points: 5) { |_| true }
23
+ backend.add_achievement(id: 'm', title: 'Monkey', description: '', points: 5) { false }
24
+ backend.do_frame(FakeCore.new)
25
+
26
+ win = Gemba::AchievementsWindow.new(
27
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
28
+ )
29
+ win.update_game(rom_id: nil, backend: backend)
30
+ win.show
31
+ win.refresh(backend)
32
+ app.update
33
+
34
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
35
+ items = app.tcl_eval("#{tree} children {}").split
36
+ titles = items.map { |id| app.tcl_eval("#{tree} set #{id} name") }
37
+ assert_equal 'Aardvark', titles[0], "earned should be first"
38
+ assert_equal 'Monkey', titles[1], "Monkey before Zebra alphabetically"
39
+ assert_equal 'Zebra', titles[2]
40
+ ensure
41
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_click_name_heading_sorts_asc_with_indicator
47
+ assert_tk_app("clicking name heading sorts A→Z and shows ▲") do
48
+ begin
49
+ require "gemba/headless"
50
+ require "support/achievements_window_helpers"
51
+ Gemba.bus = Gemba::EventBus.new
52
+
53
+ backend = Gemba::Achievements::FakeBackend.new
54
+ backend.login_with_token(username: 'u', token: 't')
55
+ backend.add_achievement(id: 'z', title: 'Zebra', description: '', points: 5) { false }
56
+ backend.add_achievement(id: 'a', title: 'Aardvark', description: '', points: 5) { false }
57
+ backend.add_achievement(id: 'm', title: 'Monkey', description: '', points: 5) { false }
58
+
59
+ win = Gemba::AchievementsWindow.new(
60
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
61
+ )
62
+ win.update_game(rom_id: nil, backend: backend)
63
+ win.show
64
+ win.refresh(backend)
65
+ app.update
66
+
67
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
68
+ cmd = app.tcl_eval("#{tree} heading name -command")
69
+ app.tcl_eval("uplevel #0 {#{cmd}}")
70
+ app.update
71
+
72
+ items = app.tcl_eval("#{tree} children {}").split
73
+ titles = items.map { |id| app.tcl_eval("#{tree} set #{id} name") }
74
+ assert_equal %w[Aardvark Monkey Zebra], titles
75
+
76
+ assert_includes app.tcl_eval("#{tree} heading name -text"), '▲'
77
+ ensure
78
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
79
+ end
80
+ end
81
+ end
82
+
83
+ def test_click_name_heading_twice_reverses_to_desc
84
+ assert_tk_app("clicking name heading twice reverses to Z→A and shows ▼") do
85
+ begin
86
+ require "gemba/headless"
87
+ require "support/achievements_window_helpers"
88
+ Gemba.bus = Gemba::EventBus.new
89
+
90
+ backend = Gemba::Achievements::FakeBackend.new
91
+ backend.login_with_token(username: 'u', token: 't')
92
+ backend.add_achievement(id: 'z', title: 'Zebra', description: '', points: 5) { false }
93
+ backend.add_achievement(id: 'a', title: 'Aardvark', description: '', points: 5) { false }
94
+
95
+ win = Gemba::AchievementsWindow.new(
96
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
97
+ )
98
+ win.update_game(rom_id: nil, backend: backend)
99
+ win.show
100
+ win.refresh(backend)
101
+ app.update
102
+
103
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
104
+ cmd = app.tcl_eval("#{tree} heading name -command")
105
+ app.tcl_eval("uplevel #0 {#{cmd}}") # asc
106
+ app.tcl_eval("uplevel #0 {#{cmd}}") # desc
107
+ app.update
108
+
109
+ items = app.tcl_eval("#{tree} children {}").split
110
+ titles = items.map { |id| app.tcl_eval("#{tree} set #{id} name") }
111
+ assert_equal %w[Zebra Aardvark], titles
112
+ assert_includes app.tcl_eval("#{tree} heading name -text"), '▼'
113
+ ensure
114
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
115
+ end
116
+ end
117
+ end
118
+
119
+ def test_click_points_heading_sorts_low_to_high
120
+ assert_tk_app("clicking points heading sorts by points low→high") do
121
+ begin
122
+ require "gemba/headless"
123
+ require "support/achievements_window_helpers"
124
+ Gemba.bus = Gemba::EventBus.new
125
+
126
+ backend = Gemba::Achievements::FakeBackend.new
127
+ backend.login_with_token(username: 'u', token: 't')
128
+ backend.add_achievement(id: 'a', title: 'High', description: '', points: 50) { false }
129
+ backend.add_achievement(id: 'b', title: 'Low', description: '', points: 5) { false }
130
+ backend.add_achievement(id: 'c', title: 'Mid', description: '', points: 25) { false }
131
+
132
+ win = Gemba::AchievementsWindow.new(
133
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
134
+ )
135
+ win.update_game(rom_id: nil, backend: backend)
136
+ win.show
137
+ win.refresh(backend)
138
+ app.update
139
+
140
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
141
+ cmd = app.tcl_eval("#{tree} heading points -command")
142
+ app.tcl_eval("uplevel #0 {#{cmd}}")
143
+ app.update
144
+
145
+ items = app.tcl_eval("#{tree} children {}").split
146
+ pts = items.map { |id| app.tcl_eval("#{tree} set #{id} points").to_i }
147
+ assert_equal [5, 25, 50], pts
148
+ ensure
149
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
150
+ end
151
+ end
152
+ end
153
+
154
+ def test_click_earned_heading_puts_unearned_last
155
+ assert_tk_app("clicking earned heading keeps unearned rows at the bottom") do
156
+ begin
157
+ require "gemba/headless"
158
+ require "support/achievements_window_helpers"
159
+ require "support/fake_core"
160
+ Gemba.bus = Gemba::EventBus.new
161
+
162
+ backend = Gemba::Achievements::FakeBackend.new
163
+ backend.login_with_token(username: 'u', token: 't')
164
+ backend.add_achievement(id: 'e', title: 'Earned', description: '', points: 5) { |_| true }
165
+ backend.add_achievement(id: 'u', title: 'Unearned', description: '', points: 5) { false }
166
+ backend.do_frame(FakeCore.new)
167
+
168
+ win = Gemba::AchievementsWindow.new(
169
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
170
+ )
171
+ win.update_game(rom_id: nil, backend: backend)
172
+ win.show
173
+ win.refresh(backend)
174
+ app.update
175
+
176
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
177
+ cmd = app.tcl_eval("#{tree} heading earned -command")
178
+ app.tcl_eval("uplevel #0 {#{cmd}}")
179
+ app.update
180
+
181
+ items = app.tcl_eval("#{tree} children {}").split
182
+ earned = items.map { |id| app.tcl_eval("#{tree} set #{id} earned") }
183
+ assert_equal '', earned.last, "unearned must be last"
184
+ refute_equal '', earned.first, "earned must be first"
185
+ ensure
186
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
187
+ end
188
+ end
189
+ end
190
+
191
+ def test_switching_sort_column_clears_old_indicator
192
+ assert_tk_app("switching sort column removes indicator from previously sorted column") do
193
+ begin
194
+ require "gemba/headless"
195
+ require "support/achievements_window_helpers"
196
+ Gemba.bus = Gemba::EventBus.new
197
+
198
+ backend = Gemba::Achievements::FakeBackend.new
199
+ backend.login_with_token(username: 'u', token: 't')
200
+ backend.add_achievement(id: 'a', title: 'Alpha', description: '', points: 5) { false }
201
+
202
+ win = Gemba::AchievementsWindow.new(
203
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
204
+ )
205
+ win.update_game(rom_id: nil, backend: backend)
206
+ win.show
207
+ win.refresh(backend)
208
+ app.update
209
+
210
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
211
+ name_cmd = app.tcl_eval("#{tree} heading name -command")
212
+ pts_cmd = app.tcl_eval("#{tree} heading points -command")
213
+ app.tcl_eval("uplevel #0 {#{name_cmd}}") # sort by name
214
+ app.tcl_eval("uplevel #0 {#{pts_cmd}}") # switch to points
215
+ app.update
216
+
217
+ name_text = app.tcl_eval("#{tree} heading name -text")
218
+ pts_text = app.tcl_eval("#{tree} heading points -text")
219
+ refute_includes name_text, '▲', "name heading must lose indicator after switching"
220
+ refute_includes name_text, '▼', "name heading must lose indicator after switching"
221
+ assert_includes pts_text, '▲', "points heading should show ▲"
222
+ ensure
223
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "../shared/tk_test_helper"
5
+
6
+ # Tests for AchievementsWindow treeview rendering — achievement rows, earned/
7
+ # unearned column display, and status bar earned count.
8
+ class TestAchievementsWindowTreeRendering < Minitest::Test
9
+ include TeekTestHelper
10
+
11
+ def test_tree_shows_achievement_titles
12
+ assert_tk_app("treeview rows show achievement titles from the backend") do
13
+ begin
14
+ require "gemba/headless"
15
+ require "support/achievements_window_helpers"
16
+ Gemba.bus = Gemba::EventBus.new
17
+
18
+ backend = Gemba::Achievements::FakeBackend.new
19
+ backend.login_with_token(username: 'u', token: 't')
20
+ backend.add_achievement(id: 'a1', title: 'First Act', description: '', points: 5) { false }
21
+ backend.add_achievement(id: 'a2', title: 'Second Act', description: '', points: 10) { false }
22
+
23
+ win = Gemba::AchievementsWindow.new(
24
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
25
+ )
26
+ win.update_game(rom_id: nil, backend: backend)
27
+ win.show
28
+ win.refresh(backend)
29
+ app.update
30
+
31
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
32
+ items = app.tcl_eval("#{tree} children {}").split
33
+ titles = items.map { |id| app.tcl_eval("#{tree} set #{id} name") }
34
+ assert_equal 2, items.size
35
+ assert_includes titles, 'First Act'
36
+ assert_includes titles, 'Second Act'
37
+ ensure
38
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
39
+ end
40
+ end
41
+ end
42
+
43
+ def test_unearned_achievements_have_empty_earned_column
44
+ assert_tk_app("unearned achievements show empty earned column") do
45
+ begin
46
+ require "gemba/headless"
47
+ require "support/achievements_window_helpers"
48
+ Gemba.bus = Gemba::EventBus.new
49
+
50
+ backend = Gemba::Achievements::FakeBackend.new
51
+ backend.login_with_token(username: 'u', token: 't')
52
+ backend.add_achievement(id: 'a1', title: 'Unearned', description: '', points: 5) { false }
53
+
54
+ win = Gemba::AchievementsWindow.new(
55
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
56
+ )
57
+ win.update_game(rom_id: nil, backend: backend)
58
+ win.show
59
+ win.refresh(backend)
60
+ app.update
61
+
62
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
63
+ item = app.tcl_eval("#{tree} children {}").split.first
64
+ assert_equal '', app.tcl_eval("#{tree} set #{item} earned")
65
+ ensure
66
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
67
+ end
68
+ end
69
+ end
70
+
71
+ def test_earned_achievements_show_date
72
+ assert_tk_app("earned achievements show a YYYY-MM-DD date in the earned column") do
73
+ begin
74
+ require "gemba/headless"
75
+ require "support/achievements_window_helpers"
76
+ require "support/fake_core"
77
+ Gemba.bus = Gemba::EventBus.new
78
+
79
+ backend = Gemba::Achievements::FakeBackend.new
80
+ backend.login_with_token(username: 'u', token: 't')
81
+ backend.add_achievement(id: 'a1', title: 'Earned One', description: '', points: 5) { |_| true }
82
+ backend.do_frame(FakeCore.new)
83
+
84
+ win = Gemba::AchievementsWindow.new(
85
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
86
+ )
87
+ win.update_game(rom_id: nil, backend: backend)
88
+ win.show
89
+ win.refresh(backend)
90
+ app.update
91
+
92
+ tree = "#{Gemba::AchievementsWindow::TOP}.tf.tree"
93
+ item = app.tcl_eval("#{tree} children {}").split.first
94
+ earned = app.tcl_eval("#{tree} set #{item} earned")
95
+ assert_match(/\d{4}-\d{2}-\d{2}/, earned)
96
+ ensure
97
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
98
+ end
99
+ end
100
+ end
101
+
102
+ def test_status_bar_shows_earned_count
103
+ assert_tk_app("status bar shows 'X / Y earned'") do
104
+ begin
105
+ require "gemba/headless"
106
+ require "support/achievements_window_helpers"
107
+ require "support/fake_core"
108
+ Gemba.bus = Gemba::EventBus.new
109
+
110
+ backend = Gemba::Achievements::FakeBackend.new
111
+ backend.login_with_token(username: 'u', token: 't')
112
+ backend.add_achievement(id: 'a1', title: 'Alpha', description: '', points: 5) { |_| true }
113
+ backend.add_achievement(id: 'a2', title: 'Beta', description: '', points: 5) { false }
114
+ backend.do_frame(FakeCore.new)
115
+
116
+ win = Gemba::AchievementsWindow.new(
117
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
118
+ )
119
+ win.update_game(rom_id: nil, backend: backend)
120
+ win.show
121
+ win.refresh(backend)
122
+ app.update
123
+
124
+ top = Gemba::AchievementsWindow::TOP
125
+ status = app.command("#{top}.status_bar.status", :cget, '-text').to_s
126
+ assert_includes status, '1'
127
+ assert_includes status, '2'
128
+ ensure
129
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
130
+ end
131
+ end
132
+ end
133
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -67,7 +67,7 @@ class Teek::TestWorker
67
67
  end
68
68
 
69
69
  # Default timeout for test execution (can be overridden via TK_TEST_TIMEOUT env var)
70
- DEFAULT_TIMEOUT = 60
70
+ DEFAULT_TIMEOUT = 10
71
71
 
72
72
  def run_test(code, pipe_capture: false, timeout: nil, source_file: nil, source_line: nil)
73
73
  start unless running?
@@ -171,6 +171,22 @@ class Teek::TestWorker
171
171
  end
172
172
  result
173
173
  end
174
+
175
+ # Suppress tk_popup for the duration of the block so right-click bindings
176
+ # can build and configure menus without posting them. This avoids the
177
+ # platform grab that would block app.update on macOS and Windows.
178
+ #
179
+ # Uses Tcl `catch` so setup and teardown are idempotent — safe even if a
180
+ # previous test left _orig_tk_popup stranded due to an error.
181
+ def override_tk_popup
182
+ @app.tcl_eval("rename tk_popup _orig_tk_popup; proc tk_popup {args} {}")
183
+ yield
184
+ ensure
185
+ # Delete the no-op proc first (rename to {} = delete in Tcl), then
186
+ # restore the real tk_popup. Both wrapped in catch so a double-ensure
187
+ # or other edge case never raises.
188
+ @app.tcl_eval("catch {rename tk_popup {}}; catch {rename _orig_tk_popup tk_popup}")
189
+ end
174
190
  end
175
191
 
176
192
  # Server-side: runs in subprocess