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.
- checksums.yaml +4 -4
- data/THIRD_PARTY_NOTICES +37 -2
- data/assets/placeholder_boxart.png +0 -0
- data/bin/gemba +2 -2
- data/ext/gemba/extconf.rb +23 -1
- data/ext/gemba/gemba_ext.c +436 -2
- data/ext/gemba/gemba_ext.h +2 -0
- data/gemba.gemspec +5 -3
- data/lib/gemba/achievements/achievement.rb +23 -0
- data/lib/gemba/achievements/backend.rb +190 -0
- data/lib/gemba/achievements/cache.rb +70 -0
- data/lib/gemba/achievements/credentials_presenter.rb +142 -0
- data/lib/gemba/achievements/fake_backend.rb +205 -0
- data/lib/gemba/achievements/null_backend.rb +11 -0
- data/lib/gemba/achievements/offline_backend.rb +168 -0
- data/lib/gemba/achievements/retro_achievements/backend.rb +511 -0
- data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
- data/lib/gemba/achievements/retro_achievements/ping_worker.rb +28 -0
- data/lib/gemba/achievements/retro_achievements/unlock_retry_worker.rb +35 -0
- data/lib/gemba/achievements.rb +19 -0
- data/lib/gemba/achievements_window.rb +556 -0
- data/lib/gemba/app_controller.rb +1036 -0
- data/lib/gemba/bios.rb +54 -0
- data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
- data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
- data/lib/gemba/boxart_fetcher.rb +79 -0
- data/lib/gemba/bus_emitter.rb +13 -0
- data/lib/gemba/child_window.rb +24 -1
- data/lib/gemba/cli/commands/config_cmd.rb +83 -0
- data/lib/gemba/cli/commands/decode.rb +154 -0
- data/lib/gemba/cli/commands/patch.rb +78 -0
- data/lib/gemba/cli/commands/play.rb +78 -0
- data/lib/gemba/cli/commands/record.rb +114 -0
- data/lib/gemba/cli/commands/replay.rb +161 -0
- data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
- data/lib/gemba/cli/commands/version.rb +22 -0
- data/lib/gemba/cli.rb +52 -364
- data/lib/gemba/config.rb +154 -1
- data/lib/gemba/data/gb_games.json +1 -0
- data/lib/gemba/data/gb_md5.json +1 -0
- data/lib/gemba/data/gba_games.json +1 -0
- data/lib/gemba/data/gba_md5.json +1 -0
- data/lib/gemba/data/gbc_games.json +1 -0
- data/lib/gemba/data/gbc_md5.json +1 -0
- data/lib/gemba/emulator_frame.rb +1084 -0
- data/lib/gemba/event_bus.rb +48 -0
- data/lib/gemba/frame_stack.rb +70 -0
- data/lib/gemba/game_index.rb +84 -0
- data/lib/gemba/game_picker_frame.rb +309 -0
- data/lib/gemba/gamepad_map.rb +103 -0
- data/lib/gemba/headless.rb +6 -5
- data/lib/gemba/headless_player.rb +33 -3
- data/lib/gemba/help_window.rb +61 -0
- data/lib/gemba/hotkey_map.rb +3 -1
- data/lib/gemba/input_recorder.rb +107 -0
- data/lib/gemba/input_replayer.rb +119 -0
- data/lib/gemba/keyboard_map.rb +90 -0
- data/lib/gemba/list_picker_frame.rb +271 -0
- data/lib/gemba/locales/en.yml +109 -5
- data/lib/gemba/locales/ja.yml +109 -5
- data/lib/gemba/main_window.rb +56 -0
- data/lib/gemba/modal_stack.rb +81 -0
- data/lib/gemba/patcher_window.rb +223 -0
- data/lib/gemba/platform/gb.rb +21 -0
- data/lib/gemba/platform/gba.rb +21 -0
- data/lib/gemba/platform/gbc.rb +23 -0
- data/lib/gemba/platform.rb +20 -0
- data/lib/gemba/platform_open.rb +19 -0
- data/lib/gemba/recorder.rb +4 -3
- data/lib/gemba/replay_player.rb +691 -0
- data/lib/gemba/rom_info.rb +57 -0
- data/lib/gemba/rom_info_window.rb +16 -3
- data/lib/gemba/rom_library.rb +106 -0
- data/lib/gemba/rom_overrides.rb +47 -0
- data/lib/gemba/rom_patcher/bps.rb +161 -0
- data/lib/gemba/rom_patcher/ips.rb +101 -0
- data/lib/gemba/rom_patcher/ups.rb +119 -0
- data/lib/gemba/rom_patcher.rb +109 -0
- data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
- data/lib/gemba/runtime.rb +59 -26
- data/lib/gemba/save_state_manager.rb +4 -7
- data/lib/gemba/save_state_picker.rb +17 -4
- data/lib/gemba/session_logger.rb +64 -0
- data/lib/gemba/settings/audio_tab.rb +77 -0
- data/lib/gemba/settings/gamepad_tab.rb +351 -0
- data/lib/gemba/settings/hotkeys_tab.rb +259 -0
- data/lib/gemba/settings/paths.rb +11 -0
- data/lib/gemba/settings/recording_tab.rb +83 -0
- data/lib/gemba/settings/save_states_tab.rb +91 -0
- data/lib/gemba/settings/system_tab.rb +377 -0
- data/lib/gemba/settings/video_tab.rb +318 -0
- data/lib/gemba/settings_window.rb +162 -1036
- data/lib/gemba/version.rb +1 -1
- data/lib/gemba/virtual_keyboard.rb +19 -0
- data/lib/gemba.rb +2 -12
- data/test/achievements_window/test_bulk_sync.rb +218 -0
- data/test/achievements_window/test_bus_events.rb +125 -0
- data/test/achievements_window/test_close_confirmation.rb +201 -0
- data/test/achievements_window/test_initial_state.rb +164 -0
- data/test/achievements_window/test_sorting.rb +227 -0
- data/test/achievements_window/test_tree_rendering.rb +133 -0
- data/test/fixtures/fake_bios.bin +0 -0
- data/test/fixtures/pong.gba +0 -0
- data/test/fixtures/test.gb +0 -0
- data/test/fixtures/test.gbc +0 -0
- data/test/fixtures/test_quicksave.ss +0 -0
- data/test/screenshots/no_focus.png +0 -0
- data/test/shared/teek_test_worker.rb +17 -1
- data/test/shared/tk_test_helper.rb +92 -4
- data/test/support/achievements_window_helpers.rb +18 -0
- data/test/support/fake_core.rb +25 -0
- data/test/support/fake_ra_runtime.rb +74 -0
- data/test/support/fake_requester.rb +78 -0
- data/test/support/player_helpers.rb +20 -5
- data/test/test_achievement.rb +32 -0
- data/test/{test_player.rb → test_app_controller.rb} +353 -85
- data/test/test_bios.rb +123 -0
- data/test/test_boxart_fetcher.rb +150 -0
- data/test/test_cli.rb +17 -265
- data/test/test_cli_config.rb +64 -0
- data/test/test_cli_decode.rb +97 -0
- data/test/test_cli_patch.rb +58 -0
- data/test/test_cli_play.rb +213 -0
- data/test/test_cli_ra.rb +175 -0
- data/test/test_cli_record.rb +69 -0
- data/test/test_cli_replay.rb +72 -0
- data/test/test_cli_sync_requester.rb +152 -0
- data/test/test_cli_version.rb +27 -0
- data/test/test_config.rb +3 -3
- data/test/test_config_ra.rb +69 -0
- data/test/test_core.rb +62 -1
- data/test/test_credentials_presenter.rb +192 -0
- data/test/test_event_bus.rb +100 -0
- data/test/test_fake_backend_achievements.rb +130 -0
- data/test/test_fake_backend_auth.rb +68 -0
- data/test/test_game_index.rb +77 -0
- data/test/test_game_picker_frame.rb +310 -0
- data/test/test_gamepad_map.rb +1 -3
- data/test/test_headless_player.rb +17 -3
- data/test/test_help_window.rb +82 -0
- data/test/test_hotkey_map.rb +22 -1
- data/test/test_input_recorder.rb +179 -0
- data/test/test_input_replay_determinism.rb +113 -0
- data/test/test_input_replayer.rb +162 -0
- data/test/test_keyboard_map.rb +1 -3
- data/test/test_libretro_backend.rb +41 -0
- data/test/test_list_picker_frame.rb +391 -0
- data/test/test_locale.rb +1 -1
- data/test/test_logging.rb +123 -0
- data/test/test_null_backend.rb +42 -0
- data/test/test_offline_backend.rb +116 -0
- data/test/test_overlay_renderer.rb +1 -1
- data/test/test_platform.rb +149 -0
- data/test/test_ra_backend.rb +313 -0
- data/test/test_ra_backend_unlock_gate.rb +56 -0
- data/test/test_ra_backend_unlock_retry.rb +123 -0
- data/test/test_recorder.rb +0 -3
- data/test/test_replay_player.rb +316 -0
- data/test/test_rom_info.rb +149 -0
- data/test/test_rom_overrides.rb +86 -0
- data/test/test_rom_patcher.rb +383 -0
- data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
- data/test/test_save_state_manager.rb +2 -4
- data/test/test_settings_audio.rb +107 -0
- data/test/test_settings_hotkeys.rb +83 -66
- data/test/test_settings_recording.rb +49 -0
- data/test/test_settings_save_states.rb +97 -0
- data/test/test_settings_system.rb +133 -0
- data/test/test_settings_video.rb +450 -0
- data/test/test_settings_window.rb +76 -507
- data/test/test_tip_service.rb +6 -6
- data/test/test_toast_overlay.rb +1 -1
- data/test/test_virtual_events.rb +221 -0
- data/test/test_virtual_keyboard.rb +1 -1
- data/vendor/rcheevos/CHANGELOG.md +495 -0
- data/vendor/rcheevos/LICENSE +21 -0
- data/vendor/rcheevos/Package.swift +33 -0
- data/vendor/rcheevos/README.md +67 -0
- data/vendor/rcheevos/include/module.modulemap +70 -0
- data/vendor/rcheevos/include/rc_api_editor.h +296 -0
- data/vendor/rcheevos/include/rc_api_info.h +280 -0
- data/vendor/rcheevos/include/rc_api_request.h +77 -0
- data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
- data/vendor/rcheevos/include/rc_api_user.h +262 -0
- data/vendor/rcheevos/include/rc_client.h +877 -0
- data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
- data/vendor/rcheevos/include/rc_consoles.h +138 -0
- data/vendor/rcheevos/include/rc_error.h +59 -0
- data/vendor/rcheevos/include/rc_export.h +100 -0
- data/vendor/rcheevos/include/rc_hash.h +200 -0
- data/vendor/rcheevos/include/rc_runtime.h +148 -0
- data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
- data/vendor/rcheevos/include/rc_util.h +51 -0
- data/vendor/rcheevos/include/rcheevos.h +8 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
- data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
- data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
- data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
- data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
- data/vendor/rcheevos/src/rc_client.c +6941 -0
- data/vendor/rcheevos/src/rc_client_external.c +281 -0
- data/vendor/rcheevos/src/rc_client_external.h +177 -0
- data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
- data/vendor/rcheevos/src/rc_client_internal.h +409 -0
- data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
- data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
- data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
- data/vendor/rcheevos/src/rc_compat.c +251 -0
- data/vendor/rcheevos/src/rc_compat.h +121 -0
- data/vendor/rcheevos/src/rc_libretro.c +915 -0
- data/vendor/rcheevos/src/rc_libretro.h +98 -0
- data/vendor/rcheevos/src/rc_util.c +199 -0
- data/vendor/rcheevos/src/rc_version.c +11 -0
- data/vendor/rcheevos/src/rc_version.h +32 -0
- data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
- data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
- data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
- data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
- data/vendor/rcheevos/src/rcheevos/format.c +330 -0
- data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
- data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
- data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
- data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
- data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
- data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
- data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
- data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
- data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
- data/vendor/rcheevos/src/rcheevos/value.c +935 -0
- data/vendor/rcheevos/src/rhash/aes.c +480 -0
- data/vendor/rcheevos/src/rhash/aes.h +49 -0
- data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
- data/vendor/rcheevos/src/rhash/hash.c +1402 -0
- data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
- data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
- data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
- data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
- data/vendor/rcheevos/src/rhash/md5.c +382 -0
- data/vendor/rcheevos/src/rhash/md5.h +91 -0
- data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
- data/vendor/rcheevos/test/libretro.h +205 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
- data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
- data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
- data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
- data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
- data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
- data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
- data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
- data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
- data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
- data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
- data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
- data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
- data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
- data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
- data/vendor/rcheevos/test/rhash/data.c +657 -0
- data/vendor/rcheevos/test/rhash/data.h +32 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
- data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
- data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
- data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
- data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
- data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
- data/vendor/rcheevos/test/test.c +113 -0
- data/vendor/rcheevos/test/test_framework.h +205 -0
- data/vendor/rcheevos/test/test_rc_client.c +10509 -0
- data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
- data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
- data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
- data/vendor/rcheevos/test/test_types.natvis +9 -0
- data/vendor/rcheevos/validator/validator.c +658 -0
- data/vendor/rcheevos/validator/validator.vcxproj +152 -0
- data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
- metadata +277 -10
- data/lib/gemba/input_mappings.rb +0 -214
- 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
|
@@ -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
|