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
data/lib/gemba/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemba
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Gemba
6
+ # Virtual keyboard device that tracks key press/release state.
7
+ # Presents the same interface as an SDL gamepad: +button?+ and +closed?+.
8
+ class VirtualKeyboard
9
+ def initialize
10
+ @held = Set.new
11
+ end
12
+
13
+ def press(keysym) = @held.add(keysym)
14
+ def release(keysym) = @held.delete(keysym)
15
+ def button?(keysym) = @held.include?(keysym)
16
+ def closed? = false
17
+ end
18
+
19
+ end
data/lib/gemba.rb CHANGED
@@ -1,17 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "gemba/version"
3
4
  require "teek"
4
5
  require "teek/sdl2"
5
6
  require_relative "gemba/runtime"
6
- require_relative "gemba/child_window"
7
- require_relative "gemba/tip_service"
8
- require_relative "gemba/settings_window"
9
- require_relative "gemba/rom_info_window"
10
- require_relative "gemba/save_state_picker"
11
- require_relative "gemba/save_state_manager"
12
- require_relative "gemba/toast_overlay"
13
- require_relative "gemba/overlay_renderer"
14
- require_relative "gemba/input_mappings"
15
- require_relative "gemba/hotkey_map"
16
- require_relative "gemba/recorder"
17
- require_relative "gemba/player"
7
+ require_relative "gemba/platform_open"
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "../shared/tk_test_helper"
5
+
6
+ # Tests for AchievementsWindow bulk sync — unofficial toggle fires bus event,
7
+ # UI locks/unlocks during sync, per-game status updates, and done count display.
8
+ class TestAchievementsWindowBulkSync < Minitest::Test
9
+ include TeekTestHelper
10
+
11
+ def test_unofficial_toggle_fires_bus_event
12
+ assert_tk_app("toggling unofficial checkbox fires :ra_unofficial_changed on the bus") do
13
+ begin
14
+ require "gemba/headless"
15
+ require "support/achievements_window_helpers"
16
+ require "tmpdir"
17
+ Gemba.bus = Gemba::EventBus.new
18
+
19
+ received = nil
20
+ Gemba.bus.on(:ra_unofficial_changed) { |value:, **| received = value }
21
+
22
+ backend = Gemba::Achievements::FakeBackend.new
23
+ backend.login_with_token(username: 'u', token: 't')
24
+ backend.stub_fetch_for_display([])
25
+
26
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
27
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
28
+ win = Gemba::AchievementsWindow.new(
29
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
30
+ )
31
+ win.update_game(rom_id: nil, backend: backend)
32
+ win.show
33
+ app.update
34
+
35
+ top = Gemba::AchievementsWindow::TOP
36
+ app.command("#{top}.toolbar.unofficial", 'invoke')
37
+ app.update
38
+
39
+ assert_equal true, received, ":ra_unofficial_changed should fire with true"
40
+ ensure
41
+ ENV.delete('GEMBA_CONFIG_DIR')
42
+ end
43
+ ensure
44
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
45
+ end
46
+ end
47
+ end
48
+
49
+ def test_unofficial_toggle_locks_ui_during_sync
50
+ assert_tk_app("unofficial toggle disables both buttons while bulk sync runs") do
51
+ begin
52
+ require "gemba/headless"
53
+ require "support/achievements_window_helpers"
54
+ require "tmpdir"
55
+ Gemba.bus = Gemba::EventBus.new
56
+
57
+ backend = Gemba::Achievements::FakeBackend.new
58
+ backend.login_with_token(username: 'u', token: 't')
59
+
60
+ # Capture widget states from INSIDE the fetch callback — FakeBackend
61
+ # calls synchronously so we're mid-sync right here.
62
+ mid_sync_states = {}
63
+ backend.stub_fetch_for_display do |_|
64
+ top = Gemba::AchievementsWindow::TOP
65
+ mid_sync_states[:sync] = app.command("#{top}.toolbar.sync", :cget, '-state').to_s
66
+ mid_sync_states[:unoff] = app.command("#{top}.toolbar.unofficial", :cget, '-state').to_s
67
+ nil
68
+ end
69
+
70
+ roms = [make_rom_entry(id: 'r1', title: 'Game')]
71
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
72
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
73
+ win = Gemba::AchievementsWindow.new(
74
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
75
+ )
76
+ win.update_game(rom_id: nil, backend: backend)
77
+ win.show
78
+ app.update
79
+
80
+ top = Gemba::AchievementsWindow::TOP
81
+ app.command("#{top}.toolbar.unofficial", 'invoke')
82
+ app.update
83
+
84
+ assert_equal 'disabled', mid_sync_states[:sync], "sync button locked during bulk sync"
85
+ assert_equal 'disabled', mid_sync_states[:unoff], "unofficial button locked during bulk sync"
86
+ ensure
87
+ ENV.delete('GEMBA_CONFIG_DIR')
88
+ end
89
+ ensure
90
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
91
+ end
92
+ end
93
+ end
94
+
95
+ def test_bulk_sync_shows_per_game_status_updates
96
+ assert_tk_app("bulk sync updates the status bar for each game in sequence") do
97
+ begin
98
+ require "gemba/headless"
99
+ require "support/achievements_window_helpers"
100
+ require "tmpdir"
101
+ Gemba.bus = Gemba::EventBus.new
102
+
103
+ backend = Gemba::Achievements::FakeBackend.new
104
+ backend.login_with_token(username: 'u', token: 't')
105
+
106
+ statuses = []
107
+ backend.stub_fetch_for_display do |_|
108
+ top = Gemba::AchievementsWindow::TOP
109
+ statuses << app.command("#{top}.status_bar.status", :cget, '-text').to_s
110
+ []
111
+ end
112
+
113
+ roms = [
114
+ make_rom_entry(id: 'r1', title: 'Game One'),
115
+ make_rom_entry(id: 'r2', title: 'Game Two'),
116
+ ]
117
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
118
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
119
+ win = Gemba::AchievementsWindow.new(
120
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
121
+ )
122
+ win.update_game(rom_id: nil, backend: backend)
123
+ win.show
124
+ app.update
125
+
126
+ top = Gemba::AchievementsWindow::TOP
127
+ app.command("#{top}.toolbar.unofficial", 'invoke')
128
+ app.update
129
+
130
+ assert_equal 2, statuses.size, "one status update per game"
131
+ assert_includes statuses[0], 'Game One', "first update names Game One"
132
+ assert_includes statuses[1], 'Game Two', "second update names Game Two"
133
+ ensure
134
+ ENV.delete('GEMBA_CONFIG_DIR')
135
+ end
136
+ ensure
137
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
138
+ end
139
+ end
140
+ end
141
+
142
+ def test_bulk_sync_unlocks_ui_when_complete
143
+ assert_tk_app("bulk sync re-enables buttons after all games are done") do
144
+ begin
145
+ require "gemba/headless"
146
+ require "support/achievements_window_helpers"
147
+ require "tmpdir"
148
+ Gemba.bus = Gemba::EventBus.new
149
+
150
+ backend = Gemba::Achievements::FakeBackend.new
151
+ backend.login_with_token(username: 'u', token: 't')
152
+ backend.stub_fetch_for_display([])
153
+
154
+ roms = [make_rom_entry(id: 'r1', title: 'Game')]
155
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
156
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
157
+ win = Gemba::AchievementsWindow.new(
158
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
159
+ )
160
+ win.update_game(rom_id: nil, backend: backend)
161
+ win.show
162
+ app.update
163
+
164
+ top = Gemba::AchievementsWindow::TOP
165
+ app.command("#{top}.toolbar.unofficial", 'invoke')
166
+ app.update
167
+
168
+ assert_equal 'normal', app.command("#{top}.toolbar.sync", :cget, '-state').to_s
169
+ assert_equal 'normal', app.command("#{top}.toolbar.unofficial", :cget, '-state').to_s
170
+ ensure
171
+ ENV.delete('GEMBA_CONFIG_DIR')
172
+ end
173
+ ensure
174
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
175
+ end
176
+ end
177
+ end
178
+
179
+ def test_bulk_sync_done_status_shows_count
180
+ assert_tk_app("status bar shows N-game count after bulk sync completes") do
181
+ begin
182
+ require "gemba/headless"
183
+ require "support/achievements_window_helpers"
184
+ require "tmpdir"
185
+ Gemba.bus = Gemba::EventBus.new
186
+
187
+ backend = Gemba::Achievements::FakeBackend.new
188
+ backend.login_with_token(username: 'u', token: 't')
189
+ backend.stub_fetch_for_display([])
190
+
191
+ roms = [
192
+ make_rom_entry(id: 'r1', title: 'Game A'),
193
+ make_rom_entry(id: 'r2', title: 'Game B'),
194
+ ]
195
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
196
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
197
+ win = Gemba::AchievementsWindow.new(
198
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
199
+ )
200
+ win.update_game(rom_id: nil, backend: backend)
201
+ win.show
202
+ app.update
203
+
204
+ top = Gemba::AchievementsWindow::TOP
205
+ app.command("#{top}.toolbar.unofficial", 'invoke')
206
+ app.update
207
+
208
+ status = app.command("#{top}.status_bar.status", :cget, '-text').to_s
209
+ assert_includes status, '2', "done status should mention 2 games synced"
210
+ ensure
211
+ ENV.delete('GEMBA_CONFIG_DIR')
212
+ end
213
+ ensure
214
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "../shared/tk_test_helper"
5
+
6
+ # Tests for AchievementsWindow event bus subscriptions — sync started/done
7
+ # disables/enables the sync button and auth-result logout disables it.
8
+ class TestAchievementsWindowBusEvents < Minitest::Test
9
+ include TeekTestHelper
10
+
11
+ def test_ra_sync_started_disables_sync_button
12
+ assert_tk_app(":ra_sync_started disables the sync button") 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
+ win = Gemba::AchievementsWindow.new(
21
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
22
+ )
23
+ win.update_game(rom_id: nil, backend: backend)
24
+ win.show
25
+ app.update
26
+
27
+ Gemba.bus.emit(:ra_sync_started)
28
+ app.update
29
+
30
+ top = Gemba::AchievementsWindow::TOP
31
+ assert_equal 'disabled',
32
+ app.command("#{top}.toolbar.sync", :cget, '-state').to_s
33
+ ensure
34
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
35
+ end
36
+ end
37
+ end
38
+
39
+ def test_ra_sync_done_ok_re_enables_sync_button
40
+ assert_tk_app(":ra_sync_done(ok: true) re-enables the sync button") do
41
+ begin
42
+ require "gemba/headless"
43
+ require "support/achievements_window_helpers"
44
+ Gemba.bus = Gemba::EventBus.new
45
+
46
+ backend = Gemba::Achievements::FakeBackend.new
47
+ backend.login_with_token(username: 'u', token: 't')
48
+ win = Gemba::AchievementsWindow.new(
49
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
50
+ )
51
+ win.update_game(rom_id: nil, backend: backend)
52
+ win.show
53
+ app.update
54
+
55
+ Gemba.bus.emit(:ra_sync_started)
56
+ app.update
57
+ Gemba.bus.emit(:ra_sync_done, ok: true)
58
+ app.update
59
+
60
+ top = Gemba::AchievementsWindow::TOP
61
+ assert_equal 'normal',
62
+ app.command("#{top}.toolbar.sync", :cget, '-state').to_s
63
+ ensure
64
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
65
+ end
66
+ end
67
+ end
68
+
69
+ def test_ra_sync_done_fail_shows_error_status
70
+ assert_tk_app(":ra_sync_done(ok: false) shows sync_failed in status bar") do
71
+ begin
72
+ require "gemba/headless"
73
+ require "support/achievements_window_helpers"
74
+ Gemba.bus = Gemba::EventBus.new
75
+
76
+ backend = Gemba::Achievements::FakeBackend.new
77
+ backend.login_with_token(username: 'u', token: 't')
78
+ win = Gemba::AchievementsWindow.new(
79
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
80
+ )
81
+ win.update_game(rom_id: nil, backend: backend)
82
+ win.show
83
+ app.update
84
+
85
+ Gemba.bus.emit(:ra_sync_done, ok: false)
86
+ app.update
87
+
88
+ top = Gemba::AchievementsWindow::TOP
89
+ status = app.command("#{top}.status_bar.status", :cget, '-text').to_s
90
+ assert_equal Gemba::Locale.translate('achievements.sync_failed'), status
91
+ ensure
92
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
93
+ end
94
+ end
95
+ end
96
+
97
+ def test_ra_auth_result_logout_disables_sync_button
98
+ assert_tk_app(":ra_auth_result after logout disables sync button") do
99
+ begin
100
+ require "gemba/headless"
101
+ require "support/achievements_window_helpers"
102
+ Gemba.bus = Gemba::EventBus.new
103
+
104
+ backend = Gemba::Achievements::FakeBackend.new
105
+ backend.login_with_token(username: 'u', token: 't')
106
+ win = Gemba::AchievementsWindow.new(
107
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
108
+ )
109
+ win.update_game(rom_id: nil, backend: backend)
110
+ win.show
111
+ app.update
112
+
113
+ backend.logout
114
+ Gemba.bus.emit(:ra_auth_result, status: :logout)
115
+ app.update
116
+
117
+ top = Gemba::AchievementsWindow::TOP
118
+ assert_equal 'disabled',
119
+ app.command("#{top}.toolbar.sync", :cget, '-state').to_s
120
+ ensure
121
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "../shared/tk_test_helper"
5
+
6
+ # Tests for AchievementsWindow close-during-bulk-sync confirmation dialog —
7
+ # no dialog when not syncing, dialog shown mid-sync, 'no' keeps window open,
8
+ # 'yes' stops sync and withdraws the window.
9
+ class TestAchievementsWindowCloseConfirmation < Minitest::Test
10
+ include TeekTestHelper
11
+
12
+ def test_hide_without_bulk_sync_does_not_show_dialog
13
+ assert_tk_app("win.hide when not syncing closes without showing any dialog") do
14
+ begin
15
+ require "gemba/headless"
16
+ require "support/achievements_window_helpers"
17
+ Gemba.bus = Gemba::EventBus.new
18
+
19
+ app.tcl_eval("set ::ach_dlg 0")
20
+ app.tcl_eval("proc tk_messageBox {args} { incr ::ach_dlg; return yes }")
21
+
22
+ backend = Gemba::Achievements::FakeBackend.new
23
+ backend.login_with_token(username: 'u', token: 't')
24
+ win = Gemba::AchievementsWindow.new(
25
+ app: app, rom_library: make_rom_library, config: FakeConfig.new(false)
26
+ )
27
+ win.update_game(rom_id: nil, backend: backend)
28
+ win.show
29
+ app.update
30
+
31
+ win.hide
32
+ app.update
33
+
34
+ assert_equal '0', app.tcl_eval("set ::ach_dlg").strip,
35
+ "tk_messageBox must not be called when not in bulk sync"
36
+ ensure
37
+ app.tcl_eval("catch {rename tk_messageBox {}}") rescue nil
38
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
39
+ end
40
+ end
41
+ end
42
+
43
+ def test_hide_during_bulk_sync_shows_dialog
44
+ assert_tk_app("win.hide mid-sync triggers a tk_messageBox confirmation") do
45
+ begin
46
+ require "gemba/headless"
47
+ require "support/achievements_window_helpers"
48
+ require "tmpdir"
49
+ Gemba.bus = Gemba::EventBus.new
50
+
51
+ app.tcl_eval("set ::ach_dlg 0")
52
+ app.tcl_eval("proc tk_messageBox {args} { incr ::ach_dlg; return no }")
53
+
54
+ backend = Gemba::Achievements::FakeBackend.new
55
+ backend.login_with_token(username: 'u', token: 't')
56
+
57
+ win = nil
58
+ attempted = false
59
+ backend.stub_fetch_for_display do |_|
60
+ unless attempted
61
+ attempted = true
62
+ win.hide # dialog returns "no" → stays open
63
+ app.update
64
+ end
65
+ nil
66
+ end
67
+
68
+ roms = [make_rom_entry(id: 'r1', title: 'Game')]
69
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
70
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
71
+ win = Gemba::AchievementsWindow.new(
72
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
73
+ )
74
+ win.update_game(rom_id: nil, backend: backend)
75
+ win.show
76
+ app.update
77
+
78
+ top = Gemba::AchievementsWindow::TOP
79
+ app.command("#{top}.toolbar.unofficial", 'invoke')
80
+ app.update
81
+
82
+ assert_equal '1', app.tcl_eval("set ::ach_dlg").strip,
83
+ "tk_messageBox must be shown when hiding during bulk sync"
84
+ ensure
85
+ ENV.delete('GEMBA_CONFIG_DIR')
86
+ end
87
+ ensure
88
+ app.tcl_eval("catch {rename tk_messageBox {}}") rescue nil
89
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
90
+ end
91
+ end
92
+ end
93
+
94
+ def test_hide_confirm_no_keeps_window_visible
95
+ assert_tk_app("answering 'no' to the cancel-sync dialog keeps the window open") do
96
+ begin
97
+ require "gemba/headless"
98
+ require "support/achievements_window_helpers"
99
+ require "tmpdir"
100
+ Gemba.bus = Gemba::EventBus.new
101
+
102
+ app.tcl_eval("proc tk_messageBox {args} { return no }")
103
+
104
+ backend = Gemba::Achievements::FakeBackend.new
105
+ backend.login_with_token(username: 'u', token: 't')
106
+
107
+ win = nil
108
+ attempted = false
109
+ backend.stub_fetch_for_display do |_|
110
+ unless attempted
111
+ attempted = true
112
+ win.hide
113
+ app.update
114
+ end
115
+ nil
116
+ end
117
+
118
+ roms = [make_rom_entry(id: 'r1', title: 'Game')]
119
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
120
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
121
+ win = Gemba::AchievementsWindow.new(
122
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
123
+ )
124
+ win.update_game(rom_id: nil, backend: backend)
125
+ win.show
126
+ app.update
127
+
128
+ top = Gemba::AchievementsWindow::TOP
129
+ app.command("#{top}.toolbar.unofficial", 'invoke')
130
+ app.update
131
+
132
+ # wm state is unreliable in xvfb when the root is withdrawn; instead
133
+ # verify the sync completed and UI was unlocked — only possible if
134
+ # the window stayed open and the sync ran to completion.
135
+ assert_equal 'normal', app.command("#{top}.toolbar.sync", :cget, '-state').to_s,
136
+ "sync button must be re-enabled after sync completes (window stayed open)"
137
+ ensure
138
+ ENV.delete('GEMBA_CONFIG_DIR')
139
+ end
140
+ ensure
141
+ app.tcl_eval("catch {rename tk_messageBox {}}") rescue nil
142
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
143
+ end
144
+ end
145
+ end
146
+
147
+ def test_hide_confirm_yes_stops_sync_and_withdraws_window
148
+ assert_tk_app("answering 'yes' stops the bulk sync and withdraws the window") do
149
+ begin
150
+ require "gemba/headless"
151
+ require "support/achievements_window_helpers"
152
+ require "tmpdir"
153
+ Gemba.bus = Gemba::EventBus.new
154
+
155
+ app.tcl_eval("proc tk_messageBox {args} { return yes }")
156
+
157
+ win = nil
158
+ fetch_count = 0
159
+ hide_done = false
160
+ backend = Gemba::Achievements::FakeBackend.new
161
+ backend.login_with_token(username: 'u', token: 't')
162
+ backend.stub_fetch_for_display do |_|
163
+ fetch_count += 1
164
+ unless hide_done
165
+ hide_done = true
166
+ win.hide # dialog returns "yes" → cancel + withdraw
167
+ app.update
168
+ end
169
+ nil
170
+ end
171
+
172
+ roms = [
173
+ make_rom_entry(id: 'r1', title: 'Game One'),
174
+ make_rom_entry(id: 'r2', title: 'Game Two'),
175
+ ]
176
+ Dir.mktmpdir("ach_win_test") do |tmpdir|
177
+ ENV['GEMBA_CONFIG_DIR'] = tmpdir
178
+ win = Gemba::AchievementsWindow.new(
179
+ app: app, rom_library: make_rom_library(*roms), config: FakeConfig.new(false)
180
+ )
181
+ win.update_game(rom_id: nil, backend: backend)
182
+ win.show
183
+ app.update
184
+
185
+ top = Gemba::AchievementsWindow::TOP
186
+ app.command("#{top}.toolbar.unofficial", 'invoke')
187
+ app.update
188
+
189
+ assert_equal 1, fetch_count, "sync must stop after the first game when user says yes"
190
+ assert_equal 'withdrawn', app.tcl_eval("wm state #{top}").strip,
191
+ "window must be withdrawn after user confirms close"
192
+ ensure
193
+ ENV.delete('GEMBA_CONFIG_DIR')
194
+ end
195
+ ensure
196
+ app.tcl_eval("catch {rename tk_messageBox {}}") rescue nil
197
+ app.command(:destroy, Gemba::AchievementsWindow::TOP) rescue nil
198
+ end
199
+ end
200
+ end
201
+ end