gemba 0.2.0 → 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.
@@ -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
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require_relative "support/fake_ra_runtime"
6
+ require_relative "support/fake_requester"
7
+ require_relative "support/fake_core"
8
+
9
+ # Tests for the unlock retry queue in RetroAchievements::Backend.
10
+ #
11
+ # FakeRequester fires on_progress synchronously so no event loop is needed.
12
+ # For worker-style calls (drain_unlock_queue) it yields [ok, id] to match
13
+ # what UnlockRetryWorker produces in production.
14
+ class TestRABackendUnlockRetry < Minitest::Test
15
+ Backend = Gemba::Achievements::RetroAchievements::Backend
16
+
17
+ PATCH = {
18
+ "PatchData" => {
19
+ "RichPresencePatch" => "",
20
+ "Achievements" => [
21
+ { "ID" => 101, "Title" => "First Blood", "Description" => "Kill",
22
+ "Points" => 5, "MemAddr" => "0=1", "Flags" => 3 },
23
+ { "ID" => 102, "Title" => "Survivor", "Description" => "Survive",
24
+ "Points" => 10, "MemAddr" => "1=1", "Flags" => 3 },
25
+ ],
26
+ },
27
+ }.freeze
28
+
29
+ def setup
30
+ @rt = FakeRARuntime.new
31
+ @req = FakeRequester.new
32
+ @b = Backend.new(app: nil, runtime: @rt, requester: @req)
33
+ end
34
+
35
+ def login_and_load
36
+ @req.stub(r: "login2", body: { "Success" => true })
37
+ @req.stub(r: "gameid", body: { "GameID" => 42 })
38
+ @req.stub(r: "patch", body: PATCH)
39
+ @req.stub(r: "unlocks", body: { "Success" => true, "UserUnlocks" => [] })
40
+ @b.login_with_token(username: "user", token: "tok")
41
+ Dir.mktmpdir do |dir|
42
+ rom = File.join(dir, "test.gba")
43
+ File.write(rom, "FAKEGBA")
44
+ @b.load_game(nil, rom, "deadbeef" * 4)
45
+ end
46
+ end
47
+
48
+ # -- Queue builds on initial failure ----------------------------------------
49
+
50
+ def test_failed_unlock_enqueues_for_retry
51
+ login_and_load
52
+ @req.stub(r: "awardachievement", ok: false, body: { "Success" => false })
53
+ @rt.queue_triggers("101")
54
+ @b.do_frame(FakeCore.new)
55
+ assert_equal 1, @b.unlock_queue.size
56
+ assert_equal "101", @b.unlock_queue.first[:id]
57
+ end
58
+
59
+ def test_successful_unlock_does_not_enqueue
60
+ login_and_load
61
+ @req.stub(r: "awardachievement", ok: true, body: { "Success" => true })
62
+ @rt.queue_triggers("101")
63
+ @b.do_frame(FakeCore.new)
64
+ assert_empty @b.unlock_queue
65
+ end
66
+
67
+ def test_multiple_failed_unlocks_all_enqueue
68
+ login_and_load
69
+ @req.stub(r: "awardachievement", ok: false, body: { "Success" => false })
70
+ @rt.queue_triggers("101", "102")
71
+ @b.do_frame(FakeCore.new)
72
+ assert_equal 2, @b.unlock_queue.size
73
+ end
74
+
75
+ # -- drain_unlock_queue -----------------------------------------------------
76
+
77
+ def test_drain_sends_retry_request_per_entry
78
+ login_and_load
79
+ @req.stub(r: "awardachievement", ok: true, body: { "Success" => true })
80
+ @b.unlock_queue << { id: "101", hardcore: false }
81
+ @b.unlock_queue << { id: "102", hardcore: false }
82
+ @b.drain_unlock_queue
83
+ assert_equal 2, @req.requests_for("awardachievement").size
84
+ end
85
+
86
+ def test_drain_clears_queue_on_success
87
+ login_and_load
88
+ @req.stub(r: "awardachievement", ok: true, body: { "Success" => true })
89
+ @b.unlock_queue << { id: "101", hardcore: false }
90
+ @b.drain_unlock_queue
91
+ assert_empty @b.unlock_queue
92
+ end
93
+
94
+ def test_drain_keeps_queue_on_failure
95
+ login_and_load
96
+ @req.stub(r: "awardachievement", ok: false, body: { "Success" => false })
97
+ @b.unlock_queue << { id: "101", hardcore: false }
98
+ @b.drain_unlock_queue
99
+ assert_equal 1, @b.unlock_queue.size
100
+ end
101
+
102
+ def test_drain_partial_success_removes_only_succeeded
103
+ login_and_load
104
+ @req.stub_queue(r: "awardachievement", ok: true, body: { "Success" => true })
105
+ @req.stub_queue(r: "awardachievement", ok: false, body: { "Success" => false })
106
+ @b.unlock_queue << { id: "101", hardcore: false }
107
+ @b.unlock_queue << { id: "102", hardcore: false }
108
+ @b.drain_unlock_queue
109
+ assert_equal 1, @b.unlock_queue.size
110
+ assert_equal "102", @b.unlock_queue.first[:id]
111
+ end
112
+
113
+ # -- shutdown ---------------------------------------------------------------
114
+
115
+ def test_shutdown_logs_pending_and_does_not_raise
116
+ @b.unlock_queue << { id: "101", hardcore: false }
117
+ @b.shutdown
118
+ end
119
+
120
+ def test_shutdown_with_empty_queue_does_not_raise
121
+ @b.shutdown
122
+ end
123
+ end
@@ -70,7 +70,7 @@ class TestRomPatcher < Minitest::Test
70
70
  payload + [src_crc, tgt_crc, patch_crc].pack("VVV")
71
71
  end
72
72
 
73
- # Encode a UPS varint (simple bitshift encoding).
73
+ # Encode a UPS varint (additive-shift encoding with continuation bias).
74
74
  def ups_varint(n)
75
75
  out = "".b
76
76
  loop do
@@ -81,6 +81,7 @@ class TestRomPatcher < Minitest::Test
81
81
  break
82
82
  end
83
83
  out << x.chr
84
+ n -= 1
84
85
  end
85
86
  out
86
87
  end
@@ -87,6 +87,71 @@ class TestVirtualEvents < Minitest::Test
87
87
  assert_virtual_event_fires('ToggleHelpWindow', with_rom: false)
88
88
  end
89
89
 
90
+ # ── ListPickerFrame <<RightClick>> virtual event ───────────────────────────
91
+
92
+ def test_double_button1_on_list_picker_tree_fires_double_click_virtual_event
93
+ assert_tk_app("<Double-Button-1> on list picker treeview generates <<DoubleClick>>") do
94
+ require "gemba/headless"
95
+
96
+ roms = [{ 'title' => 'Test', 'platform' => 'gba', 'path' => '/t.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.show
101
+ app.update
102
+
103
+ tree = '.list_picker.tree'
104
+ app.tcl_eval("bind #{tree} <<DoubleClick>> {+set ::dclick_fired 1}")
105
+
106
+ iid = app.tcl_eval("#{tree} children {}").split.first
107
+ app.tcl_eval("#{tree} focus #{iid}")
108
+ app.tcl_eval("#{tree} selection set #{iid}")
109
+ # Simulate double-click via the virtual event directly (event generate
110
+ # <Double-Button-1> is forbidden in Tk 9).
111
+ app.tcl_eval("event generate #{tree} <<DoubleClick>>")
112
+ app.update
113
+
114
+ fired = app.tcl_eval("info exists ::dclick_fired") rescue "0"
115
+ assert_equal "1", fired, "<<DoubleClick>> binding did not fire"
116
+
117
+ app.command(:destroy, '.list_picker') rescue nil
118
+ end
119
+ end
120
+
121
+ def test_button3_on_list_picker_tree_fires_right_click_virtual_event
122
+ assert_tk_app("<Button-3> on list picker treeview generates <<RightClick>>") do
123
+ require "gemba/headless"
124
+
125
+ roms = [{ 'title' => 'Test', 'platform' => 'gba', 'path' => '/t.gba' }]
126
+ lib = Struct.new(:roms) { def all = roms }.new(roms)
127
+ picker = Gemba::ListPickerFrame.new(app: app, rom_library: lib)
128
+ picker.show
129
+ app.show
130
+ app.update
131
+ app.tcl_eval("raise .")
132
+ app.tcl_eval("update idletasks")
133
+
134
+ tree = '.list_picker.tree'
135
+ # Append a Tcl side-effect to <<RightClick>> on the tree
136
+ app.tcl_eval("bind #{tree} <<RightClick>> {+set ::rclick_fired 1}")
137
+
138
+ iid = app.tcl_eval("#{tree} children {}").split.first
139
+ bbox = app.tcl_eval("#{tree} bbox #{iid}").split.map(&:to_i)
140
+ bx = bbox[0] + 5
141
+ by = bbox[1] + 5
142
+
143
+ override_tk_popup do
144
+ app.tcl_eval("event generate #{tree} <Button-3> -x #{bx} -y #{by} -warp 1")
145
+ app.update
146
+ end
147
+
148
+ fired = app.tcl_eval("info exists ::rclick_fired") rescue "0"
149
+ assert_equal "1", fired, "<<RightClick>> was not generated by <Button-3>"
150
+
151
+ app.command(:destroy, '.list_picker') rescue nil
152
+ end
153
+ end
154
+
90
155
  private
91
156
 
92
157
  # Verifies that generating <<EventName>> on '.' fires the binding.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Cook
@@ -194,6 +194,7 @@ files:
194
194
  - lib/gemba/achievements/retro_achievements/backend.rb
195
195
  - lib/gemba/achievements/retro_achievements/cli_sync_requester.rb
196
196
  - lib/gemba/achievements/retro_achievements/ping_worker.rb
197
+ - lib/gemba/achievements/retro_achievements/unlock_retry_worker.rb
197
198
  - lib/gemba/achievements_window.rb
198
199
  - lib/gemba/app_controller.rb
199
200
  - lib/gemba/bios.rb
@@ -232,6 +233,7 @@ files:
232
233
  - lib/gemba/input_recorder.rb
233
234
  - lib/gemba/input_replayer.rb
234
235
  - lib/gemba/keyboard_map.rb
236
+ - lib/gemba/list_picker_frame.rb
235
237
  - lib/gemba/locale.rb
236
238
  - lib/gemba/locales/en.yml
237
239
  - lib/gemba/locales/ja.yml
@@ -331,6 +333,7 @@ files:
331
333
  - test/test_input_replayer.rb
332
334
  - test/test_keyboard_map.rb
333
335
  - test/test_libretro_backend.rb
336
+ - test/test_list_picker_frame.rb
334
337
  - test/test_locale.rb
335
338
  - test/test_logging.rb
336
339
  - test/test_mgba.rb
@@ -340,6 +343,7 @@ files:
340
343
  - test/test_platform.rb
341
344
  - test/test_ra_backend.rb
342
345
  - test/test_ra_backend_unlock_gate.rb
346
+ - test/test_ra_backend_unlock_retry.rb
343
347
  - test/test_recorder.rb
344
348
  - test/test_replay_player.rb
345
349
  - test/test_rom_info.rb
@@ -491,7 +495,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
491
495
  requirements:
492
496
  - libmgba development headers
493
497
  - rubyzip gem >= 2.4 (optional, for loading ROMs from .zip files)
494
- rubygems_version: 3.6.9
498
+ rubygems_version: 4.0.3
495
499
  specification_version: 4
496
500
  summary: GBA emulator frontend powered by teek and libmgba
497
501
  test_files: []