teek-ui 0.1.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 (127) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +452 -0
  4. data/lib/teek/ui/canvas_item.rb +270 -0
  5. data/lib/teek/ui/component_handle.rb +32 -0
  6. data/lib/teek/ui/document.rb +185 -0
  7. data/lib/teek/ui/errors.rb +41 -0
  8. data/lib/teek/ui/event_binding.rb +22 -0
  9. data/lib/teek/ui/event_bus.rb +53 -0
  10. data/lib/teek/ui/grid_validator.rb +89 -0
  11. data/lib/teek/ui/handle.rb +585 -0
  12. data/lib/teek/ui/image.rb +58 -0
  13. data/lib/teek/ui/keysyms.rb +72 -0
  14. data/lib/teek/ui/menu_builder.rb +130 -0
  15. data/lib/teek/ui/menu_entry_addressing.rb +62 -0
  16. data/lib/teek/ui/modal_stack.rb +99 -0
  17. data/lib/teek/ui/mouse_events.rb +31 -0
  18. data/lib/teek/ui/node.rb +141 -0
  19. data/lib/teek/ui/option_dump_parsing.rb +31 -0
  20. data/lib/teek/ui/overlay_anchors.rb +29 -0
  21. data/lib/teek/ui/overlay_validator.rb +33 -0
  22. data/lib/teek/ui/pane_validator.rb +30 -0
  23. data/lib/teek/ui/realized_node.rb +25 -0
  24. data/lib/teek/ui/realizer.rb +577 -0
  25. data/lib/teek/ui/scope.rb +40 -0
  26. data/lib/teek/ui/screens.rb +141 -0
  27. data/lib/teek/ui/session.rb +417 -0
  28. data/lib/teek/ui/tab_validator.rb +30 -0
  29. data/lib/teek/ui/text_content.rb +321 -0
  30. data/lib/teek/ui/tree_inspector.rb +97 -0
  31. data/lib/teek/ui/validator.rb +131 -0
  32. data/lib/teek/ui/var.rb +98 -0
  33. data/lib/teek/ui/version.rb +7 -0
  34. data/lib/teek/ui/widget_addressing.rb +47 -0
  35. data/lib/teek/ui/widget_dsl.rb +512 -0
  36. data/lib/teek/ui/widget_type.rb +242 -0
  37. data/lib/teek/ui/widget_types/button.rb +7 -0
  38. data/lib/teek/ui/widget_types/canvas.rb +15 -0
  39. data/lib/teek/ui/widget_types/checkbox.rb +7 -0
  40. data/lib/teek/ui/widget_types/column.rb +14 -0
  41. data/lib/teek/ui/widget_types/context_menu.rb +15 -0
  42. data/lib/teek/ui/widget_types/divider.rb +10 -0
  43. data/lib/teek/ui/widget_types/dropdown.rb +7 -0
  44. data/lib/teek/ui/widget_types/grid.rb +12 -0
  45. data/lib/teek/ui/widget_types/group.rb +7 -0
  46. data/lib/teek/ui/widget_types/label.rb +7 -0
  47. data/lib/teek/ui/widget_types/list.rb +7 -0
  48. data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
  49. data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
  50. data/lib/teek/ui/widget_types/menu_item.rb +20 -0
  51. data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
  52. data/lib/teek/ui/widget_types/number_box.rb +7 -0
  53. data/lib/teek/ui/widget_types/pane.rb +37 -0
  54. data/lib/teek/ui/widget_types/panel.rb +7 -0
  55. data/lib/teek/ui/widget_types/progress.rb +7 -0
  56. data/lib/teek/ui/widget_types/radio.rb +7 -0
  57. data/lib/teek/ui/widget_types/row.rb +14 -0
  58. data/lib/teek/ui/widget_types/scrollable.rb +17 -0
  59. data/lib/teek/ui/widget_types/slider.rb +7 -0
  60. data/lib/teek/ui/widget_types/spacer.rb +14 -0
  61. data/lib/teek/ui/widget_types/split.rb +17 -0
  62. data/lib/teek/ui/widget_types/tab.rb +35 -0
  63. data/lib/teek/ui/widget_types/table.rb +9 -0
  64. data/lib/teek/ui/widget_types/tabs.rb +10 -0
  65. data/lib/teek/ui/widget_types/text_area.rb +7 -0
  66. data/lib/teek/ui/widget_types/text_box.rb +7 -0
  67. data/lib/teek/ui/widget_types/tree.rb +9 -0
  68. data/lib/teek/ui/widget_types/window.rb +50 -0
  69. data/lib/teek/ui/widget_types.rb +121 -0
  70. data/lib/teek/ui/widget_validators.rb +62 -0
  71. data/lib/teek/ui.rb +54 -0
  72. data/teek-ui.gemspec +24 -0
  73. data/test/support/fake_app.rb +57 -0
  74. data/test/test_busy.rb +82 -0
  75. data/test/test_canvas_items.rb +551 -0
  76. data/test/test_clipboard.rb +87 -0
  77. data/test/test_close_handling.rb +118 -0
  78. data/test/test_component.rb +151 -0
  79. data/test/test_component_realtk.rb +120 -0
  80. data/test/test_debug_info.rb +158 -0
  81. data/test/test_document.rb +317 -0
  82. data/test/test_event_bus.rb +163 -0
  83. data/test/test_events.rb +164 -0
  84. data/test/test_fake_app_contract.rb +62 -0
  85. data/test/test_grid.rb +97 -0
  86. data/test/test_handle.rb +386 -0
  87. data/test/test_handle_destroy_realtk.rb +344 -0
  88. data/test/test_helper.rb +20 -0
  89. data/test/test_image.rb +24 -0
  90. data/test/test_image_realtk.rb +146 -0
  91. data/test/test_incremental_realize.rb +203 -0
  92. data/test/test_keysyms.rb +76 -0
  93. data/test/test_layout.rb +185 -0
  94. data/test/test_lazy_realize.rb +128 -0
  95. data/test/test_managed_window.rb +304 -0
  96. data/test/test_menu_dsl.rb +230 -0
  97. data/test/test_menu_entry_addressing.rb +73 -0
  98. data/test/test_menu_realize.rb +296 -0
  99. data/test/test_modal_handle.rb +147 -0
  100. data/test/test_modal_stack.rb +219 -0
  101. data/test/test_modal_stack_realtk.rb +131 -0
  102. data/test/test_native_scrollable.rb +250 -0
  103. data/test/test_node.rb +193 -0
  104. data/test/test_overlay.rb +139 -0
  105. data/test/test_raw.rb +58 -0
  106. data/test/test_reactive_vars.rb +118 -0
  107. data/test/test_realizer.rb +264 -0
  108. data/test/test_scope.rb +38 -0
  109. data/test/test_screens.rb +260 -0
  110. data/test/test_screens_realtk.rb +314 -0
  111. data/test/test_scrollable.rb +185 -0
  112. data/test/test_scrollable_wheel.rb +172 -0
  113. data/test/test_scrollbar_auto_hide.rb +127 -0
  114. data/test/test_split.rb +145 -0
  115. data/test/test_tabs.rb +201 -0
  116. data/test/test_text_content.rb +664 -0
  117. data/test/test_toast.rb +126 -0
  118. data/test/test_tree_inspector.rb +183 -0
  119. data/test/test_ui.rb +428 -0
  120. data/test/test_validator.rb +209 -0
  121. data/test/test_var.rb +32 -0
  122. data/test/test_widget_dsl.rb +621 -0
  123. data/test/test_widget_introspection.rb +126 -0
  124. data/test/test_widget_types.rb +429 -0
  125. data/test/test_widget_types_realtk.rb +82 -0
  126. data/test/test_widget_validators.rb +82 -0
  127. metadata +207 -0
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/screens'
5
+
6
+ # Pure stack-bookkeeping tests, no Tk involved - Screens' reveal/conceal
7
+ # logic (pack/pack-forget for a panel, show/hide for a window) is exercised
8
+ # against real Tk widgets in test_screens_realtk.rb instead.
9
+ class TestScreens < Minitest::Test
10
+ FakePanelScreen = Struct.new(:app, :path) do
11
+ def type
12
+ :panel
13
+ end
14
+ end
15
+
16
+ FakeWindowScreen = Struct.new(:shows, :hides) do
17
+ def initialize(shows = [], hides = [])
18
+ super
19
+ end
20
+
21
+ def type
22
+ :window
23
+ end
24
+
25
+ def show
26
+ shows << true
27
+ end
28
+
29
+ def hide
30
+ hides << true
31
+ end
32
+ end
33
+
34
+ FakeLazyScreen = Struct.new(:app, :path, :realized_flag, :realize_calls) do
35
+ def initialize(app, path, realized_flag = false)
36
+ super(app, path, realized_flag, [])
37
+ end
38
+
39
+ def type
40
+ :panel
41
+ end
42
+
43
+ def realized?
44
+ realized_flag
45
+ end
46
+
47
+ def realize!(document)
48
+ realize_calls << document
49
+ self.realized_flag = true
50
+ end
51
+ end
52
+
53
+ def test_starts_empty
54
+ screens = Teek::UI::Screens.new
55
+
56
+ refute screens.active?
57
+ assert_nil screens.current
58
+ assert_nil screens.current_screen
59
+ assert_equal 0, screens.size
60
+ end
61
+
62
+ def test_push_activates_and_tracks_current
63
+ screens = Teek::UI::Screens.new
64
+ picker = FakePanelScreen.new(FakeApp.new, '.picker')
65
+
66
+ screens.push(:picker, picker)
67
+
68
+ assert screens.active?
69
+ assert_equal :picker, screens.current
70
+ assert_same picker, screens.current_screen
71
+ assert_equal 1, screens.size
72
+ end
73
+
74
+ def test_push_reveals_a_panel_screen_by_packing_it_to_fill_its_parent
75
+ screens = Teek::UI::Screens.new
76
+ app = FakeApp.new
77
+ picker = FakePanelScreen.new(app, '.picker')
78
+
79
+ screens.push(:picker, picker)
80
+
81
+ assert_equal [[[:pack, '.picker'], { fill: :both, expand: 1 }]], app.calls
82
+ end
83
+
84
+ def test_push_reveals_a_window_screen_via_show
85
+ screens = Teek::UI::Screens.new
86
+ settings = FakeWindowScreen.new
87
+
88
+ screens.push(:settings, settings)
89
+
90
+ assert_equal [true], settings.shows
91
+ assert_equal [], settings.hides
92
+ end
93
+
94
+ def test_pushing_a_second_screen_conceals_the_first
95
+ screens = Teek::UI::Screens.new
96
+ app = FakeApp.new
97
+ picker = FakePanelScreen.new(app, '.picker')
98
+ emulator = FakePanelScreen.new(app, '.emulator')
99
+
100
+ screens.push(:picker, picker)
101
+ screens.push(:emulator, emulator)
102
+
103
+ assert_equal :emulator, screens.current
104
+ assert_equal 2, screens.size
105
+ assert_equal [
106
+ [[:pack, '.picker'], { fill: :both, expand: 1 }],
107
+ [[:pack, :forget, '.picker'], {}],
108
+ [[:pack, '.emulator'], { fill: :both, expand: 1 }],
109
+ ], app.calls
110
+ end
111
+
112
+ def test_pop_conceals_the_current_and_reveals_the_one_underneath
113
+ screens = Teek::UI::Screens.new
114
+ app = FakeApp.new
115
+ picker = FakePanelScreen.new(app, '.picker')
116
+ emulator = FakePanelScreen.new(app, '.emulator')
117
+ screens.push(:picker, picker)
118
+ screens.push(:emulator, emulator)
119
+ app.calls.clear
120
+
121
+ screens.pop
122
+
123
+ assert_equal :picker, screens.current
124
+ assert_equal 1, screens.size
125
+ assert_equal [
126
+ [[:pack, :forget, '.emulator'], {}],
127
+ [[:pack, '.picker'], { fill: :both, expand: 1 }],
128
+ ], app.calls
129
+ end
130
+
131
+ def test_pop_on_the_last_screen_leaves_the_stack_inactive
132
+ screens = Teek::UI::Screens.new
133
+ picker = FakePanelScreen.new(FakeApp.new, '.picker')
134
+ screens.push(:picker, picker)
135
+
136
+ screens.pop
137
+
138
+ refute screens.active?
139
+ assert_nil screens.current
140
+ assert_equal 0, screens.size
141
+ end
142
+
143
+ def test_pop_on_an_empty_stack_is_a_safe_no_op
144
+ screens = Teek::UI::Screens.new
145
+
146
+ screens.pop
147
+
148
+ refute screens.active?
149
+ end
150
+
151
+ def test_replace_current_swaps_the_screen_but_keeps_the_name_and_depth
152
+ screens = Teek::UI::Screens.new
153
+ app = FakeApp.new
154
+ picker = FakePanelScreen.new(app, '.picker')
155
+ other_picker = FakePanelScreen.new(app, '.other_picker')
156
+ screens.push(:picker, picker)
157
+ app.calls.clear
158
+
159
+ screens.replace_current(other_picker)
160
+
161
+ assert_equal :picker, screens.current
162
+ assert_same other_picker, screens.current_screen
163
+ assert_equal 1, screens.size
164
+ assert_equal [
165
+ [[:pack, :forget, '.picker'], {}],
166
+ [[:pack, '.other_picker'], { fill: :both, expand: 1 }],
167
+ ], app.calls
168
+ end
169
+
170
+ def test_replace_current_on_an_empty_stack_is_a_safe_no_op
171
+ screens = Teek::UI::Screens.new
172
+ app = FakeApp.new
173
+ picker = FakePanelScreen.new(app, '.picker')
174
+
175
+ screens.replace_current(picker)
176
+
177
+ refute screens.active?
178
+ assert_equal [], app.calls
179
+ end
180
+
181
+ def test_a_three_deep_push_pop_push_sequence_restores_the_correct_screen_at_each_step
182
+ screens = Teek::UI::Screens.new
183
+ app = FakeApp.new
184
+ picker = FakePanelScreen.new(app, '.picker')
185
+ emulator = FakePanelScreen.new(app, '.emulator')
186
+ settings = FakePanelScreen.new(app, '.settings')
187
+
188
+ screens.push(:picker, picker)
189
+ assert_equal :picker, screens.current
190
+ assert_equal 1, screens.size
191
+
192
+ screens.push(:emulator, emulator)
193
+ assert_equal :emulator, screens.current
194
+ assert_equal 2, screens.size
195
+
196
+ screens.push(:settings, settings)
197
+ assert_equal :settings, screens.current
198
+ assert_equal 3, screens.size
199
+
200
+ screens.pop
201
+ assert_equal :emulator, screens.current
202
+ assert_equal 2, screens.size
203
+
204
+ screens.pop
205
+ assert_equal :picker, screens.current
206
+ assert_equal 1, screens.size
207
+
208
+ screens.push(:settings, settings)
209
+ assert_equal :settings, screens.current
210
+ assert_equal 2, screens.size
211
+ end
212
+
213
+ def test_push_realizes_a_not_yet_realized_screen_before_revealing
214
+ screens = Teek::UI::Screens.new(document: :the_document)
215
+ app = FakeApp.new
216
+ picker = FakeLazyScreen.new(app, '.picker', false)
217
+
218
+ screens.push(:picker, picker)
219
+
220
+ assert_equal [:the_document], picker.realize_calls
221
+ assert_equal [[[:pack, '.picker'], { fill: :both, expand: 1 }]], app.calls
222
+ end
223
+
224
+ def test_push_does_not_realize_an_already_realized_screen
225
+ screens = Teek::UI::Screens.new(document: :the_document)
226
+ app = FakeApp.new
227
+ picker = FakeLazyScreen.new(app, '.picker', true)
228
+
229
+ screens.push(:picker, picker)
230
+
231
+ assert_equal [], picker.realize_calls
232
+ end
233
+
234
+ def test_push_skips_the_lazy_check_for_a_screen_that_has_no_opinion_on_it
235
+ # matches every FakePanelScreen/FakeWindowScreen test above - a screen
236
+ # with no realized?/realize! at all (the plain "already-built Handle"
237
+ # usage) behaves exactly as it always has, no document: needed either.
238
+ screens = Teek::UI::Screens.new
239
+ app = FakeApp.new
240
+ picker = FakePanelScreen.new(app, '.picker')
241
+
242
+ screens.push(:picker, picker)
243
+
244
+ assert screens.active?
245
+ end
246
+
247
+ def test_pop_returns_the_popped_screen
248
+ screens = Teek::UI::Screens.new
249
+ picker = FakePanelScreen.new(FakeApp.new, '.picker')
250
+ screens.push(:picker, picker)
251
+
252
+ assert_same picker, screens.pop
253
+ end
254
+
255
+ def test_pop_on_an_empty_stack_returns_nil
256
+ screens = Teek::UI::Screens.new
257
+
258
+ assert_nil screens.pop
259
+ end
260
+ end
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Real-Tk half of Screens' coverage - pure stack bookkeeping (current/size/
7
+ # active?, a push-pop-push sequence) is covered headlessly in
8
+ # test_screens.rb; these exercise the actual reveal/conceal side effects
9
+ # (pack/pack-forget for a panel, show/hide - including modal grab/release -
10
+ # for a window) against real widgets.
11
+ class TestScreensRealTk < Minitest::Test
12
+ include TeekTestHelper
13
+
14
+ def test_push_reveals_a_panel_screen
15
+ assert_tk_app("pushing a panel screen should pack it, mapping it") do
16
+ require 'teek/ui'
17
+
18
+ session = Teek::UI.app(title: 'Screens Test') do |ui|
19
+ ui.panel(:picker)
20
+ ui.panel(:emulator)
21
+ end
22
+ session.run_async
23
+ session.app.update
24
+
25
+ session.screens.push(:picker, session[:picker])
26
+ session.app.update
27
+
28
+ assert session.app.winfo.ismapped?(session[:picker].path)
29
+
30
+ session.app.destroy
31
+ end
32
+ end
33
+
34
+ def test_pushing_a_second_panel_screen_conceals_the_first
35
+ assert_tk_app("pushing a second screen should pack-forget the previous one") do
36
+ require 'teek/ui'
37
+
38
+ session = Teek::UI.app(title: 'Screens Test') do |ui|
39
+ ui.panel(:picker)
40
+ ui.panel(:emulator)
41
+ end
42
+ session.run_async
43
+ session.app.update
44
+
45
+ session.screens.push(:picker, session[:picker])
46
+ session.app.update
47
+ session.screens.push(:emulator, session[:emulator])
48
+ session.app.update
49
+
50
+ refute session.app.winfo.ismapped?(session[:picker].path)
51
+ assert session.app.winfo.ismapped?(session[:emulator].path)
52
+
53
+ session.app.destroy
54
+ end
55
+ end
56
+
57
+ def test_pop_conceals_the_current_panel_and_reveals_the_one_underneath
58
+ assert_tk_app("pop should hide the topmost screen and re-show whatever's underneath") do
59
+ require 'teek/ui'
60
+
61
+ session = Teek::UI.app(title: 'Screens Test') do |ui|
62
+ ui.panel(:picker)
63
+ ui.panel(:emulator)
64
+ end
65
+ session.run_async
66
+ session.app.update
67
+
68
+ session.screens.push(:picker, session[:picker])
69
+ session.screens.push(:emulator, session[:emulator])
70
+ session.app.update
71
+
72
+ session.screens.pop
73
+ session.app.update
74
+
75
+ assert session.app.winfo.ismapped?(session[:picker].path)
76
+ refute session.app.winfo.ismapped?(session[:emulator].path)
77
+ assert_equal :picker, session.screens.current
78
+
79
+ session.app.destroy
80
+ end
81
+ end
82
+
83
+ def test_replace_current_swaps_a_panel_screen_in_place
84
+ assert_tk_app("replace_current should hide the old panel, show the new one, keep the same name/depth") do
85
+ require 'teek/ui'
86
+
87
+ session = Teek::UI.app(title: 'Screens Test') do |ui|
88
+ ui.panel(:picker)
89
+ ui.panel(:other_picker)
90
+ end
91
+ session.run_async
92
+ session.app.update
93
+
94
+ session.screens.push(:picker, session[:picker])
95
+ session.app.update
96
+
97
+ session.screens.replace_current(session[:other_picker])
98
+ session.app.update
99
+
100
+ refute session.app.winfo.ismapped?(session[:picker].path)
101
+ assert session.app.winfo.ismapped?(session[:other_picker].path)
102
+ assert_equal :picker, session.screens.current
103
+ assert_equal 1, session.screens.size
104
+
105
+ session.app.destroy
106
+ end
107
+ end
108
+
109
+ def test_push_reveals_a_window_screen_via_show
110
+ assert_tk_app("pushing a window screen should show it (deiconified/mapped)") do
111
+ require 'teek/ui'
112
+
113
+ session = Teek::UI.app(title: 'Screens Test') { |ui| ui.window(:settings) }
114
+ session.run_async
115
+ session.app.update
116
+
117
+ refute session.app.winfo.ismapped?(session[:settings].path), "should start withdrawn"
118
+
119
+ session.screens.push(:settings, session[:settings])
120
+ session.app.update
121
+
122
+ assert session.app.winfo.ismapped?(session[:settings].path)
123
+
124
+ session.app.destroy
125
+ end
126
+ end
127
+
128
+ def test_pop_conceals_a_window_screen_via_hide
129
+ assert_tk_app("popping a window screen should withdraw it") do
130
+ require 'teek/ui'
131
+
132
+ session = Teek::UI.app(title: 'Screens Test') { |ui| ui.window(:settings) }
133
+ session.run_async
134
+ session.app.update
135
+
136
+ session.screens.push(:settings, session[:settings])
137
+ session.app.update
138
+
139
+ session.screens.pop
140
+ session.app.update
141
+
142
+ refute session.app.winfo.ismapped?(session[:settings].path)
143
+
144
+ session.app.destroy
145
+ end
146
+ end
147
+
148
+ def test_modal_window_screen_grabs_when_pushed_and_releases_when_popped
149
+ assert_tk_app("a modal: true window screen should grab on push, release on pop") do
150
+ require 'teek/ui'
151
+
152
+ session = Teek::UI.app(title: 'Screens Test') { |ui| ui.window(:settings, modal: true) }
153
+ session.run_async
154
+ session.app.update
155
+
156
+ session.screens.push(:settings, session[:settings])
157
+ session.app.update
158
+
159
+ path = session[:settings].path
160
+ assert_equal path, session.app.tcl_eval("grab current #{path}")
161
+
162
+ session.screens.pop
163
+ session.app.update
164
+
165
+ assert_equal '', session.app.tcl_eval("grab current #{path}")
166
+
167
+ session.app.destroy
168
+ end
169
+ end
170
+
171
+ def test_pushing_a_not_yet_realized_lazy_panel_screen_realizes_it_then_reveals_it
172
+ assert_tk_app("pushing a lazy: true panel screen should realize it on demand, then pack it") do
173
+ require 'teek/ui'
174
+
175
+ handle = nil
176
+ session = Teek::UI.app(title: 'Screens Test') do |ui|
177
+ ui.panel(:host) { |p| handle = p.panel(:picker, lazy: true) { |pk| pk.button(:load, text: 'Load') } }
178
+ end
179
+ session.run_async
180
+ session.app.update
181
+
182
+ assert_raises(Teek::UI::NotRealizedError) { handle.path }
183
+
184
+ session.screens.push(:picker, handle)
185
+ session.app.update
186
+
187
+ assert session.app.winfo.ismapped?(handle.path)
188
+ assert session.app.winfo.exists?("#{handle.path}.load")
189
+
190
+ session.app.destroy
191
+ end
192
+ end
193
+
194
+ def test_pushing_the_same_lazy_screen_again_after_a_pop_does_not_re_realize_it
195
+ assert_tk_app("popping a lazy screen (which only conceals, doesn't destroy) and pushing it again should just re-reveal the same live widget, not rebuild it") do
196
+ require 'teek/ui'
197
+
198
+ handle = nil
199
+ session = Teek::UI.app(title: 'Screens Test') do |ui|
200
+ ui.panel(:host) { |p| handle = p.panel(:picker, lazy: true) { |pk| pk.button(:load, text: 'Load') } }
201
+ end
202
+ session.run_async
203
+ session.app.update
204
+
205
+ session.screens.push(:picker, handle)
206
+ session.app.update
207
+ first_path = handle.path
208
+
209
+ session.screens.pop
210
+ session.app.update
211
+ refute session.app.winfo.ismapped?(first_path)
212
+
213
+ session.screens.push(:picker, handle)
214
+ session.app.update
215
+
216
+ assert_equal first_path, handle.path, "re-pushing the same still-realized handle should reveal the SAME widget, not build a new one at a disambiguated path"
217
+ assert session.app.winfo.ismapped?(first_path)
218
+
219
+ session.app.destroy
220
+ end
221
+ end
222
+
223
+ def test_popping_and_destroying_a_lazy_screen_releases_it_and_a_fresh_mount_can_be_pushed_again
224
+ assert_tk_app("popping a lazy screen and destroying it should tear it down; a freshly-mounted replacement should push and realize fine") do
225
+ require 'teek/ui'
226
+
227
+ build_picker = ->(ui, label) {
228
+ ui.component { |c| c.panel(:picker, lazy: true) { |p| p.button(:load, text: label).on_click { } } }
229
+ }
230
+
231
+ first_facade = nil
232
+ session = Teek::UI.app(title: 'Screens Test') do |ui|
233
+ ui.panel(:host) { |p| first_facade = build_picker.call(p, 'First') }
234
+ end
235
+ session.run_async
236
+ session.app.update
237
+ baseline = session.app.interp.callback_ids.length
238
+
239
+ first_handle = first_facade[:picker]
240
+
241
+ session.screens.push(:picker, first_handle) # realizes :picker on demand, since it isn't yet
242
+ session.app.update
243
+ assert session.app.winfo.ismapped?(first_handle.path)
244
+
245
+ popped = session.screens.pop
246
+ assert_same first_handle, popped
247
+ session.app.update
248
+ first_path = first_handle.path
249
+ refute session.app.winfo.ismapped?(first_path), "pop alone should conceal, not destroy"
250
+
251
+ popped.destroy!
252
+ session.app.update
253
+ refute session.app.winfo.exists?(first_path)
254
+ assert_equal baseline, session.app.interp.callback_ids.length
255
+
256
+ second_facade = nil
257
+ session.add(:host) { |p| second_facade = build_picker.call(p, 'Second') }
258
+ second_handle = second_facade[:picker]
259
+
260
+ session.screens.push(:picker, second_handle)
261
+ session.app.update
262
+
263
+ assert session.app.winfo.ismapped?(second_handle.path)
264
+ refute_equal first_path, second_handle.path
265
+
266
+ session.app.destroy
267
+ end
268
+ end
269
+
270
+ def test_a_child_window_style_lazy_modal_can_be_opened_and_closed_repeatedly
271
+ assert_tk_app("a lazy: true window pushed through ModalStack should realize fresh each open, cleaning up on close") do
272
+ require 'teek/ui'
273
+
274
+ build_dialog = ->(ui, label) {
275
+ ui.component { |c| c.window(:settings, lazy: true, modal: true) { |w| w.button(:ok, text: label) } }
276
+ }
277
+
278
+ session = Teek::UI.app(title: 'Screens Test') { |ui| ui.panel(:host) }
279
+ session.run_async
280
+ session.app.update
281
+ session.modal = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { }, document: session.document)
282
+
283
+ paths = []
284
+ 3.times do |i|
285
+ facade = nil
286
+ # each "open" builds a genuinely FRESH component (a new mount,
287
+ # own Scope - see WidgetDSL#component) via the same incremental-
288
+ # realize entry point any post-run dynamic UI uses - the session
289
+ # is already realized by this point, so ordinary DSL calls
290
+ # outside a session.add block would raise ClosedBuilderError.
291
+ session.add(:host) { |ui2| facade = build_dialog.call(ui2, "Open #{i + 1}") }
292
+ handle = facade[:settings]
293
+
294
+ session.modal.push(:settings, handle)
295
+ session.app.update
296
+ assert session.app.winfo.ismapped?(handle.path)
297
+ paths << handle.path
298
+
299
+ session.modal.pop.destroy!
300
+ session.app.update
301
+
302
+ # destroy! unlinks the dialog's own node from :host's children,
303
+ # not just its Tk-realized state - :host should never accumulate
304
+ # a permanently-dead child across repeated opens.
305
+ assert_equal 0, session.document.find(:host).children.length,
306
+ "each mount's dialog should be fully unlinked from :host after destroy!, not just Tk-destroyed"
307
+ end
308
+
309
+ assert_equal paths.uniq.length, paths.length, "each open should have gotten its own distinct Tk path"
310
+
311
+ session.app.destroy
312
+ end
313
+ end
314
+ end