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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/README.md +452 -0
- data/lib/teek/ui/canvas_item.rb +270 -0
- data/lib/teek/ui/component_handle.rb +32 -0
- data/lib/teek/ui/document.rb +185 -0
- data/lib/teek/ui/errors.rb +41 -0
- data/lib/teek/ui/event_binding.rb +22 -0
- data/lib/teek/ui/event_bus.rb +53 -0
- data/lib/teek/ui/grid_validator.rb +89 -0
- data/lib/teek/ui/handle.rb +585 -0
- data/lib/teek/ui/image.rb +58 -0
- data/lib/teek/ui/keysyms.rb +72 -0
- data/lib/teek/ui/menu_builder.rb +130 -0
- data/lib/teek/ui/menu_entry_addressing.rb +62 -0
- data/lib/teek/ui/modal_stack.rb +99 -0
- data/lib/teek/ui/mouse_events.rb +31 -0
- data/lib/teek/ui/node.rb +141 -0
- data/lib/teek/ui/option_dump_parsing.rb +31 -0
- data/lib/teek/ui/overlay_anchors.rb +29 -0
- data/lib/teek/ui/overlay_validator.rb +33 -0
- data/lib/teek/ui/pane_validator.rb +30 -0
- data/lib/teek/ui/realized_node.rb +25 -0
- data/lib/teek/ui/realizer.rb +577 -0
- data/lib/teek/ui/scope.rb +40 -0
- data/lib/teek/ui/screens.rb +141 -0
- data/lib/teek/ui/session.rb +417 -0
- data/lib/teek/ui/tab_validator.rb +30 -0
- data/lib/teek/ui/text_content.rb +321 -0
- data/lib/teek/ui/tree_inspector.rb +97 -0
- data/lib/teek/ui/validator.rb +131 -0
- data/lib/teek/ui/var.rb +98 -0
- data/lib/teek/ui/version.rb +7 -0
- data/lib/teek/ui/widget_addressing.rb +47 -0
- data/lib/teek/ui/widget_dsl.rb +512 -0
- data/lib/teek/ui/widget_type.rb +242 -0
- data/lib/teek/ui/widget_types/button.rb +7 -0
- data/lib/teek/ui/widget_types/canvas.rb +15 -0
- data/lib/teek/ui/widget_types/checkbox.rb +7 -0
- data/lib/teek/ui/widget_types/column.rb +14 -0
- data/lib/teek/ui/widget_types/context_menu.rb +15 -0
- data/lib/teek/ui/widget_types/divider.rb +10 -0
- data/lib/teek/ui/widget_types/dropdown.rb +7 -0
- data/lib/teek/ui/widget_types/grid.rb +12 -0
- data/lib/teek/ui/widget_types/group.rb +7 -0
- data/lib/teek/ui/widget_types/label.rb +7 -0
- data/lib/teek/ui/widget_types/list.rb +7 -0
- data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
- data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
- data/lib/teek/ui/widget_types/menu_item.rb +20 -0
- data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
- data/lib/teek/ui/widget_types/number_box.rb +7 -0
- data/lib/teek/ui/widget_types/pane.rb +37 -0
- data/lib/teek/ui/widget_types/panel.rb +7 -0
- data/lib/teek/ui/widget_types/progress.rb +7 -0
- data/lib/teek/ui/widget_types/radio.rb +7 -0
- data/lib/teek/ui/widget_types/row.rb +14 -0
- data/lib/teek/ui/widget_types/scrollable.rb +17 -0
- data/lib/teek/ui/widget_types/slider.rb +7 -0
- data/lib/teek/ui/widget_types/spacer.rb +14 -0
- data/lib/teek/ui/widget_types/split.rb +17 -0
- data/lib/teek/ui/widget_types/tab.rb +35 -0
- data/lib/teek/ui/widget_types/table.rb +9 -0
- data/lib/teek/ui/widget_types/tabs.rb +10 -0
- data/lib/teek/ui/widget_types/text_area.rb +7 -0
- data/lib/teek/ui/widget_types/text_box.rb +7 -0
- data/lib/teek/ui/widget_types/tree.rb +9 -0
- data/lib/teek/ui/widget_types/window.rb +50 -0
- data/lib/teek/ui/widget_types.rb +121 -0
- data/lib/teek/ui/widget_validators.rb +62 -0
- data/lib/teek/ui.rb +54 -0
- data/teek-ui.gemspec +24 -0
- data/test/support/fake_app.rb +57 -0
- data/test/test_busy.rb +82 -0
- data/test/test_canvas_items.rb +551 -0
- data/test/test_clipboard.rb +87 -0
- data/test/test_close_handling.rb +118 -0
- data/test/test_component.rb +151 -0
- data/test/test_component_realtk.rb +120 -0
- data/test/test_debug_info.rb +158 -0
- data/test/test_document.rb +317 -0
- data/test/test_event_bus.rb +163 -0
- data/test/test_events.rb +164 -0
- data/test/test_fake_app_contract.rb +62 -0
- data/test/test_grid.rb +97 -0
- data/test/test_handle.rb +386 -0
- data/test/test_handle_destroy_realtk.rb +344 -0
- data/test/test_helper.rb +20 -0
- data/test/test_image.rb +24 -0
- data/test/test_image_realtk.rb +146 -0
- data/test/test_incremental_realize.rb +203 -0
- data/test/test_keysyms.rb +76 -0
- data/test/test_layout.rb +185 -0
- data/test/test_lazy_realize.rb +128 -0
- data/test/test_managed_window.rb +304 -0
- data/test/test_menu_dsl.rb +230 -0
- data/test/test_menu_entry_addressing.rb +73 -0
- data/test/test_menu_realize.rb +296 -0
- data/test/test_modal_handle.rb +147 -0
- data/test/test_modal_stack.rb +219 -0
- data/test/test_modal_stack_realtk.rb +131 -0
- data/test/test_native_scrollable.rb +250 -0
- data/test/test_node.rb +193 -0
- data/test/test_overlay.rb +139 -0
- data/test/test_raw.rb +58 -0
- data/test/test_reactive_vars.rb +118 -0
- data/test/test_realizer.rb +264 -0
- data/test/test_scope.rb +38 -0
- data/test/test_screens.rb +260 -0
- data/test/test_screens_realtk.rb +314 -0
- data/test/test_scrollable.rb +185 -0
- data/test/test_scrollable_wheel.rb +172 -0
- data/test/test_scrollbar_auto_hide.rb +127 -0
- data/test/test_split.rb +145 -0
- data/test/test_tabs.rb +201 -0
- data/test/test_text_content.rb +664 -0
- data/test/test_toast.rb +126 -0
- data/test/test_tree_inspector.rb +183 -0
- data/test/test_ui.rb +428 -0
- data/test/test_validator.rb +209 -0
- data/test/test_var.rb +32 -0
- data/test/test_widget_dsl.rb +621 -0
- data/test/test_widget_introspection.rb +126 -0
- data/test/test_widget_types.rb +429 -0
- data/test/test_widget_types_realtk.rb +82 -0
- data/test/test_widget_validators.rb +82 -0
- metadata +207 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
class TestMenuRealize < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_menu_bar_attaches_to_the_root_window
|
|
10
|
+
assert_tk_app("a top-level menu_bar should attach via -menu to the root window") do
|
|
11
|
+
require 'teek/ui'
|
|
12
|
+
|
|
13
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
14
|
+
ui.menu_bar(:mb) { |mb| mb.menu(label: 'File') }
|
|
15
|
+
end
|
|
16
|
+
session.run_async
|
|
17
|
+
session.app.update
|
|
18
|
+
|
|
19
|
+
assert_equal session[:mb].path, session.app.tcl_eval('. cget -menu')
|
|
20
|
+
|
|
21
|
+
session.app.destroy
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_menu_bar_attaches_to_a_ui_window_not_just_the_root
|
|
26
|
+
assert_tk_app("a menu_bar declared inside ui.window should attach to that window, not the root") do
|
|
27
|
+
require 'teek/ui'
|
|
28
|
+
|
|
29
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
30
|
+
ui.window(:settings) { |w| w.menu_bar(:mb) { |mb| mb.menu(label: 'File') } }
|
|
31
|
+
end
|
|
32
|
+
session.run_async
|
|
33
|
+
session.app.update
|
|
34
|
+
|
|
35
|
+
settings_path = session[:settings].path
|
|
36
|
+
assert_equal session[:mb].path, session.app.tcl_eval("#{settings_path} cget -menu")
|
|
37
|
+
refute_equal session[:mb].path, session.app.tcl_eval('. cget -menu'),
|
|
38
|
+
"the root window should not also pick up the settings window's menu bar"
|
|
39
|
+
|
|
40
|
+
session.app.destroy
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_nested_menu_realizes_as_a_cascade_under_its_parents_path
|
|
45
|
+
assert_tk_app("a nested .menu should realize as a real menu widget, added as a cascade entry") do
|
|
46
|
+
require 'teek/ui'
|
|
47
|
+
|
|
48
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
49
|
+
ui.menu_bar(:mb) { |mb| mb.menu(:file, label: 'File') }
|
|
50
|
+
end
|
|
51
|
+
session.run_async
|
|
52
|
+
session.app.update
|
|
53
|
+
|
|
54
|
+
mb_path = session[:mb].path
|
|
55
|
+
file_path = session[:file].path
|
|
56
|
+
assert session.app.winfo.exists?(file_path)
|
|
57
|
+
assert file_path.start_with?("#{mb_path}."),
|
|
58
|
+
"a submenu's path (#{file_path}) should be nested under its parent menu's path (#{mb_path}) - Tk requires it"
|
|
59
|
+
|
|
60
|
+
assert_equal 'cascade', session.app.tcl_eval("#{mb_path} type 0")
|
|
61
|
+
assert_equal 'File', session.app.command(mb_path, :entrycget, 0, '-label')
|
|
62
|
+
assert_equal file_path, session.app.command(mb_path, :entrycget, 0, '-menu')
|
|
63
|
+
|
|
64
|
+
session.app.destroy
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_item_fires_its_block_on_invoke
|
|
69
|
+
assert_tk_app("a menu item's block should fire like any other command entry") do
|
|
70
|
+
require 'teek/ui'
|
|
71
|
+
|
|
72
|
+
fired = false
|
|
73
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
74
|
+
ui.menu_bar { |mb| mb.menu(:file, label: 'File') { |f| f.item(label: 'Open') { fired = true } } }
|
|
75
|
+
end
|
|
76
|
+
session.run_async
|
|
77
|
+
session.app.update
|
|
78
|
+
|
|
79
|
+
session.app.tcl_eval("#{session[:file].path} invoke 0")
|
|
80
|
+
|
|
81
|
+
assert fired, "the item's block did not fire"
|
|
82
|
+
|
|
83
|
+
session.app.destroy
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_named_item_is_addressable_via_ui_name_and_supports_enable_disable_configure
|
|
88
|
+
assert_tk_app("a named item should be addressable via ui[:name] and support .enable/.disable/.configure") do
|
|
89
|
+
require 'teek/ui'
|
|
90
|
+
|
|
91
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
92
|
+
ui.menu_bar do |mb|
|
|
93
|
+
mb.menu(:file, label: 'File') do |f|
|
|
94
|
+
f.item(label: 'New') { }
|
|
95
|
+
f.item(:quick_load, label: 'Quick Load') { }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
session.run_async
|
|
100
|
+
session.app.update
|
|
101
|
+
|
|
102
|
+
file_path = session[:file].path
|
|
103
|
+
item = session[:quick_load]
|
|
104
|
+
|
|
105
|
+
assert_equal 'normal', session.app.command(file_path, :entrycget, 1, '-state')
|
|
106
|
+
|
|
107
|
+
item.disable
|
|
108
|
+
assert_equal 'disabled', session.app.command(file_path, :entrycget, 1, '-state')
|
|
109
|
+
|
|
110
|
+
item.enable
|
|
111
|
+
assert_equal 'normal', session.app.command(file_path, :entrycget, 1, '-state')
|
|
112
|
+
|
|
113
|
+
item.configure(label: 'Load Recent Save')
|
|
114
|
+
assert_equal 'Load Recent Save', session.app.command(file_path, :entrycget, 1, '-label')
|
|
115
|
+
|
|
116
|
+
session.app.destroy
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_named_item_stays_correctly_addressed_after_an_earlier_sibling_is_removed
|
|
121
|
+
assert_tk_app("addressing a named item should stay correct even after an earlier sibling entry is removed") do
|
|
122
|
+
require 'teek/ui'
|
|
123
|
+
|
|
124
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
125
|
+
ui.menu_bar do |mb|
|
|
126
|
+
mb.menu(:file, label: 'File') do |f|
|
|
127
|
+
f.item(label: 'New') { }
|
|
128
|
+
f.item(:quick_load, label: 'Quick Load') { }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
session.run_async
|
|
133
|
+
session.app.update
|
|
134
|
+
|
|
135
|
+
file_path = session[:file].path
|
|
136
|
+
# Removing "New" shifts Quick Load's live index from 1 down to 0 -
|
|
137
|
+
# Tk itself renumbers around the delete; addressing by name should
|
|
138
|
+
# still land on the right entry with no stale cached index.
|
|
139
|
+
session.app.command(file_path, :delete, 0)
|
|
140
|
+
|
|
141
|
+
session[:quick_load].disable
|
|
142
|
+
|
|
143
|
+
assert_equal 'disabled', session.app.command(file_path, :entrycget, 0, '-state')
|
|
144
|
+
|
|
145
|
+
session.app.destroy
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_named_item_virtual_path_is_marked_and_rejected_by_a_raw_tk_call
|
|
150
|
+
assert_tk_app("a named item's .path should be marked past the real Tk boundary, and rejected if used raw") do
|
|
151
|
+
require 'teek/ui'
|
|
152
|
+
|
|
153
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
154
|
+
ui.menu_bar { |mb| mb.menu(:file, label: 'File') { |f| f.item(:quick_load, label: 'Quick Load') { } } }
|
|
155
|
+
end
|
|
156
|
+
session.run_async
|
|
157
|
+
session.app.update
|
|
158
|
+
|
|
159
|
+
file_path = session[:file].path
|
|
160
|
+
virtual_path = session[:quick_load].path
|
|
161
|
+
assert_equal "#{file_path}!quick_load", virtual_path
|
|
162
|
+
|
|
163
|
+
error = assert_raises(Teek::TclError) { session.app.tcl_eval("#{virtual_path} entrycget 0 -label") }
|
|
164
|
+
assert_match(/invalid command name/i, error.message)
|
|
165
|
+
|
|
166
|
+
session.app.destroy
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_separator_realizes_as_a_real_separator_entry
|
|
171
|
+
assert_tk_app("ui.separator should realize as a real Tk separator entry") do
|
|
172
|
+
require 'teek/ui'
|
|
173
|
+
|
|
174
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
175
|
+
ui.menu_bar { |mb| mb.menu(:file, label: 'File') { |f| f.item(label: 'Open') { }; f.separator } }
|
|
176
|
+
end
|
|
177
|
+
session.run_async
|
|
178
|
+
session.app.update
|
|
179
|
+
|
|
180
|
+
assert_equal 'separator', session.app.tcl_eval("#{session[:file].path} type 1")
|
|
181
|
+
|
|
182
|
+
session.app.destroy
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def test_checkbox_entry_stays_in_sync_with_its_bound_var
|
|
187
|
+
assert_tk_app("a menu checkbox entry bound to a var should toggle that var on invoke") do
|
|
188
|
+
require 'teek/ui'
|
|
189
|
+
|
|
190
|
+
wrap = nil
|
|
191
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
192
|
+
wrap = ui.var(false)
|
|
193
|
+
ui.menu_bar { |mb| mb.menu(:edit, label: 'Edit') { |e| e.checkbox(label: 'Word Wrap', bind: wrap) } }
|
|
194
|
+
end
|
|
195
|
+
session.run_async
|
|
196
|
+
session.app.update
|
|
197
|
+
|
|
198
|
+
assert_equal false, wrap.value
|
|
199
|
+
|
|
200
|
+
session.app.tcl_eval("#{session[:edit].path} invoke 0")
|
|
201
|
+
|
|
202
|
+
assert_equal true, wrap.value, "invoking the checkbutton entry should flip the bound var"
|
|
203
|
+
|
|
204
|
+
session.app.destroy
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_radio_entries_share_one_var_and_set_its_own_value_on_invoke
|
|
209
|
+
assert_tk_app("menu radio entries bound to the same var should set it to their own value on invoke") do
|
|
210
|
+
require 'teek/ui'
|
|
211
|
+
|
|
212
|
+
size = nil
|
|
213
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
214
|
+
size = ui.var('small')
|
|
215
|
+
ui.menu_bar do |mb|
|
|
216
|
+
mb.menu(:edit, label: 'Edit') do |e|
|
|
217
|
+
e.radio(label: 'Small', bind: size, value: 'small')
|
|
218
|
+
e.radio(label: 'Large', bind: size, value: 'large')
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
session.run_async
|
|
223
|
+
session.app.update
|
|
224
|
+
|
|
225
|
+
session.app.tcl_eval("#{session[:edit].path} invoke 1")
|
|
226
|
+
|
|
227
|
+
assert_equal 'large', size.value
|
|
228
|
+
|
|
229
|
+
session.app.destroy
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def test_rebuilding_a_menu_does_not_leak_entry_callbacks
|
|
234
|
+
assert_tk_app("rebuilding a context menu's contents on every right-click should not accumulate entry callbacks") do
|
|
235
|
+
require 'teek/ui'
|
|
236
|
+
|
|
237
|
+
session = Teek::UI.app(title: 'Menu Realize Test') { |ui| ui.panel(:host) }
|
|
238
|
+
session.run_async
|
|
239
|
+
session.app.update
|
|
240
|
+
baseline = session.app.interp.callback_ids.length
|
|
241
|
+
|
|
242
|
+
ctx = nil
|
|
243
|
+
5.times do
|
|
244
|
+
session.app.destroy(ctx.path) if ctx
|
|
245
|
+
session.add(:host) { |a| ctx = a.context_menu { |m| m.item(label: 'Delete') { }; m.item(label: 'Rename') { } } }
|
|
246
|
+
end
|
|
247
|
+
session.app.destroy(ctx.path)
|
|
248
|
+
|
|
249
|
+
assert_equal baseline, session.app.interp.callback_ids.length,
|
|
250
|
+
"rebuilding the context menu repeatedly should not accumulate callbacks"
|
|
251
|
+
|
|
252
|
+
session.app.destroy
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def test_context_menu_pops_up_at_the_right_clicked_widgets_root_coordinates
|
|
257
|
+
assert_tk_app("a widget wired via on_right_click(context_menu) should tk_popup the right menu at the click's screen coordinates") do
|
|
258
|
+
require 'teek/ui'
|
|
259
|
+
|
|
260
|
+
session = Teek::UI.app(title: 'Menu Realize Test') do |ui|
|
|
261
|
+
ctx = ui.context_menu(:ctx) { |m| m.item(label: 'Delete') { } }
|
|
262
|
+
ui.canvas(:board).on_right_click(ctx)
|
|
263
|
+
end
|
|
264
|
+
session.run_async
|
|
265
|
+
session.app.update
|
|
266
|
+
|
|
267
|
+
session.app.tcl_eval(<<~TCL)
|
|
268
|
+
proc tk_popup {args} {
|
|
269
|
+
set ::last_popup_call $args
|
|
270
|
+
}
|
|
271
|
+
TCL
|
|
272
|
+
|
|
273
|
+
session.app.tcl_eval("event generate #{session[:board].path} <Button-3> -rootx 123 -rooty 456")
|
|
274
|
+
|
|
275
|
+
captured = session.app.split_list(session.app.tcl_eval('set ::last_popup_call'))
|
|
276
|
+
assert_equal [session[:ctx].path, '123', '456'], captured
|
|
277
|
+
|
|
278
|
+
session.app.destroy
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def test_context_menu_declared_standalone_does_not_auto_attach_anywhere
|
|
283
|
+
assert_tk_app("a context_menu should exist as a real widget but never auto-attach via -menu") do
|
|
284
|
+
require 'teek/ui'
|
|
285
|
+
|
|
286
|
+
session = Teek::UI.app(title: 'Menu Realize Test') { |ui| ui.context_menu(:ctx) { |m| m.item(label: 'Delete') { } } }
|
|
287
|
+
session.run_async
|
|
288
|
+
session.app.update
|
|
289
|
+
|
|
290
|
+
assert session.app.winfo.exists?(session[:ctx].path)
|
|
291
|
+
assert_equal '', session.app.tcl_eval('. cget -menu')
|
|
292
|
+
|
|
293
|
+
session.app.destroy
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# Handle#modal/#grab_release are thin delegates to Teek::Window (see base
|
|
7
|
+
# teek's own test_modal.rb) - no grab/focus/destroy-safety-net logic is
|
|
8
|
+
# reimplemented here. These confirm the DSL layer actually wires through
|
|
9
|
+
# to the real thing end to end, using the same grab current/status style
|
|
10
|
+
# base teek's own tests use.
|
|
11
|
+
#
|
|
12
|
+
# A ui.window is withdrawn (not viewable) until shown - Tk's own `grab
|
|
13
|
+
# set` requires a viewable window, so every test below calls .show first,
|
|
14
|
+
# the same way a real caller would before making a window modal.
|
|
15
|
+
class TestModalHandle < Minitest::Test
|
|
16
|
+
include TeekTestHelper
|
|
17
|
+
|
|
18
|
+
def test_modal_grabs_and_focuses_the_window
|
|
19
|
+
assert_tk_app("a window handle's modal should grab input and force focus onto it") do
|
|
20
|
+
require 'teek/ui'
|
|
21
|
+
|
|
22
|
+
session = Teek::UI.app(title: 'Modal Handle Test') { |ui| ui.window(:settings) }
|
|
23
|
+
session.run_async
|
|
24
|
+
session.app.update
|
|
25
|
+
|
|
26
|
+
session[:settings].show
|
|
27
|
+
session[:settings].modal
|
|
28
|
+
|
|
29
|
+
path = session[:settings].path
|
|
30
|
+
assert_equal path, session.app.tcl_eval("grab current #{path}")
|
|
31
|
+
assert_equal path, session.app.tcl_eval('focus')
|
|
32
|
+
|
|
33
|
+
session[:settings].grab_release
|
|
34
|
+
session.app.destroy
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_grab_release_releases_the_grab
|
|
39
|
+
assert_tk_app("grab_release should clear the grab set by modal") do
|
|
40
|
+
require 'teek/ui'
|
|
41
|
+
|
|
42
|
+
session = Teek::UI.app(title: 'Modal Handle Test') { |ui| ui.window(:settings) }
|
|
43
|
+
session.run_async
|
|
44
|
+
session.app.update
|
|
45
|
+
|
|
46
|
+
session[:settings].show
|
|
47
|
+
session[:settings].modal
|
|
48
|
+
session[:settings].grab_release
|
|
49
|
+
|
|
50
|
+
path = session[:settings].path
|
|
51
|
+
assert_equal '', session.app.tcl_eval("grab current #{path}")
|
|
52
|
+
|
|
53
|
+
session.app.destroy
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_modal_releases_the_grab_if_the_setup_block_raises
|
|
58
|
+
assert_tk_app("modal should release the grab immediately if its setup block raises, not leave it stuck") do
|
|
59
|
+
require 'teek/ui'
|
|
60
|
+
|
|
61
|
+
session = Teek::UI.app(title: 'Modal Handle Test') { |ui| ui.window(:settings) }
|
|
62
|
+
session.run_async
|
|
63
|
+
session.app.update
|
|
64
|
+
session[:settings].show
|
|
65
|
+
|
|
66
|
+
path = session[:settings].path
|
|
67
|
+
error = assert_raises(RuntimeError) { session[:settings].modal { raise 'boom' } }
|
|
68
|
+
assert_equal 'boom', error.message
|
|
69
|
+
|
|
70
|
+
assert_equal '', session.app.tcl_eval("grab current #{path}")
|
|
71
|
+
|
|
72
|
+
session.app.destroy
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_modal_grab_persists_after_a_successful_setup_block
|
|
77
|
+
assert_tk_app("modal's grab should still be held after a successful setup block - released explicitly, not automatically") do
|
|
78
|
+
require 'teek/ui'
|
|
79
|
+
|
|
80
|
+
session = Teek::UI.app(title: 'Modal Handle Test') { |ui| ui.window(:settings) }
|
|
81
|
+
session.run_async
|
|
82
|
+
session.app.update
|
|
83
|
+
session[:settings].show
|
|
84
|
+
|
|
85
|
+
ran = false
|
|
86
|
+
session[:settings].modal { ran = true }
|
|
87
|
+
|
|
88
|
+
assert ran, "the setup block should run"
|
|
89
|
+
path = session[:settings].path
|
|
90
|
+
assert_equal path, session.app.tcl_eval("grab current #{path}")
|
|
91
|
+
|
|
92
|
+
session[:settings].grab_release
|
|
93
|
+
session.app.destroy
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_modal_releases_the_grab_if_the_window_is_destroyed_while_grabbed
|
|
98
|
+
assert_tk_app("modal should release the grab if its window is destroyed without an explicit grab_release") do
|
|
99
|
+
require 'teek/ui'
|
|
100
|
+
|
|
101
|
+
session = Teek::UI.app(title: 'Modal Handle Test') { |ui| ui.window(:settings) }
|
|
102
|
+
session.run_async
|
|
103
|
+
session.app.update
|
|
104
|
+
session[:settings].show
|
|
105
|
+
|
|
106
|
+
path = session[:settings].path
|
|
107
|
+
session[:settings].modal
|
|
108
|
+
assert_equal path, session.app.tcl_eval("grab current #{path}")
|
|
109
|
+
|
|
110
|
+
session.app.destroy(path)
|
|
111
|
+
|
|
112
|
+
assert_equal '', session.app.tcl_eval('grab current')
|
|
113
|
+
|
|
114
|
+
session.app.destroy
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_modal_raises_on_a_non_window_handle
|
|
119
|
+
assert_tk_app("modal on a non-window handle should raise a clear error") do
|
|
120
|
+
require 'teek/ui'
|
|
121
|
+
|
|
122
|
+
session = Teek::UI.app(title: 'Modal Handle Test') { |ui| ui.button(:go, text: 'Go') }
|
|
123
|
+
session.run_async
|
|
124
|
+
session.app.update
|
|
125
|
+
|
|
126
|
+
error = assert_raises(ArgumentError) { session[:go].modal }
|
|
127
|
+
assert_match(/window/i, error.message)
|
|
128
|
+
|
|
129
|
+
session.app.destroy
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_grab_release_raises_on_a_non_window_handle
|
|
134
|
+
assert_tk_app("grab_release on a non-window handle should raise a clear error") do
|
|
135
|
+
require 'teek/ui'
|
|
136
|
+
|
|
137
|
+
session = Teek::UI.app(title: 'Modal Handle Test') { |ui| ui.button(:go, text: 'Go') }
|
|
138
|
+
session.run_async
|
|
139
|
+
session.app.update
|
|
140
|
+
|
|
141
|
+
error = assert_raises(ArgumentError) { session[:go].grab_release }
|
|
142
|
+
assert_match(/window/i, error.message)
|
|
143
|
+
|
|
144
|
+
session.app.destroy
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek/ui/modal_stack'
|
|
5
|
+
|
|
6
|
+
# Pure stack-bookkeeping and callback-lifecycle tests, no Tk involved -
|
|
7
|
+
# ModalStack wraps Screens for the actual reveal/conceal, which is already
|
|
8
|
+
# covered (headlessly and against real Tk) by test_screens.rb/
|
|
9
|
+
# test_screens_realtk.rb. These focus on what's actually new here: when
|
|
10
|
+
# on_enter/on_exit/on_focus_change fire.
|
|
11
|
+
class TestModalStack < Minitest::Test
|
|
12
|
+
FakeWindow = Struct.new(:shows, :hides) do
|
|
13
|
+
def initialize(shows = [], hides = [])
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def type
|
|
18
|
+
:window
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show
|
|
22
|
+
shows << true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def hide
|
|
26
|
+
hides << true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
FakeLazyWindow = Struct.new(:shows, :hides, :realized_flag, :realize_calls) do
|
|
31
|
+
def initialize(shows = [], hides = [], realized_flag = false)
|
|
32
|
+
super(shows, hides, realized_flag, [])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def type
|
|
36
|
+
:window
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def show
|
|
40
|
+
shows << true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def hide
|
|
44
|
+
hides << true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def realized?
|
|
48
|
+
realized_flag
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def realize!(document)
|
|
52
|
+
realize_calls << document
|
|
53
|
+
self.realized_flag = true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_starts_empty
|
|
58
|
+
stack = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { })
|
|
59
|
+
|
|
60
|
+
refute stack.active?
|
|
61
|
+
assert_nil stack.current
|
|
62
|
+
assert_equal 0, stack.size
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_push_on_an_empty_stack_fires_on_enter_and_on_focus_change_and_shows_the_window
|
|
66
|
+
enters = []
|
|
67
|
+
focus_changes = []
|
|
68
|
+
settings = FakeWindow.new
|
|
69
|
+
stack = Teek::UI::ModalStack.new(
|
|
70
|
+
on_enter: ->(name) { enters << name },
|
|
71
|
+
on_exit: -> { },
|
|
72
|
+
on_focus_change: ->(name) { focus_changes << name },
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
stack.push(:settings, settings)
|
|
76
|
+
|
|
77
|
+
assert stack.active?
|
|
78
|
+
assert_equal :settings, stack.current
|
|
79
|
+
assert_equal 1, stack.size
|
|
80
|
+
assert_equal [:settings], enters
|
|
81
|
+
assert_equal [:settings], focus_changes
|
|
82
|
+
assert_equal [true], settings.shows
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_pushing_a_second_modal_does_not_fire_on_enter_again_but_hides_the_first_and_shows_the_second
|
|
86
|
+
enters = []
|
|
87
|
+
focus_changes = []
|
|
88
|
+
settings = FakeWindow.new
|
|
89
|
+
replay = FakeWindow.new
|
|
90
|
+
stack = Teek::UI::ModalStack.new(
|
|
91
|
+
on_enter: ->(name) { enters << name },
|
|
92
|
+
on_exit: -> { },
|
|
93
|
+
on_focus_change: ->(name) { focus_changes << name },
|
|
94
|
+
)
|
|
95
|
+
stack.push(:settings, settings)
|
|
96
|
+
|
|
97
|
+
stack.push(:replay, replay)
|
|
98
|
+
|
|
99
|
+
assert_equal :replay, stack.current
|
|
100
|
+
assert_equal 2, stack.size
|
|
101
|
+
assert_equal [:settings], enters, "on_enter should only fire on the empty -> non-empty transition"
|
|
102
|
+
assert_equal [:settings, :replay], focus_changes
|
|
103
|
+
assert_equal [true], settings.hides, "the previous top should be withdrawn, not dismissed"
|
|
104
|
+
assert_equal [true], replay.shows
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_pop_with_a_modal_remaining_reveals_it_and_fires_on_focus_change_not_on_exit
|
|
108
|
+
exits = 0
|
|
109
|
+
focus_changes = []
|
|
110
|
+
settings = FakeWindow.new
|
|
111
|
+
replay = FakeWindow.new
|
|
112
|
+
stack = Teek::UI::ModalStack.new(
|
|
113
|
+
on_enter: ->(_) { },
|
|
114
|
+
on_exit: -> { exits += 1 },
|
|
115
|
+
on_focus_change: ->(name) { focus_changes << name },
|
|
116
|
+
)
|
|
117
|
+
stack.push(:settings, settings)
|
|
118
|
+
stack.push(:replay, replay)
|
|
119
|
+
focus_changes.clear
|
|
120
|
+
|
|
121
|
+
stack.pop
|
|
122
|
+
|
|
123
|
+
assert_equal :settings, stack.current
|
|
124
|
+
assert_equal 1, stack.size
|
|
125
|
+
assert_equal [true], replay.hides
|
|
126
|
+
assert_equal [true, true], settings.shows, "settings should be re-shown"
|
|
127
|
+
assert_equal [:settings], focus_changes
|
|
128
|
+
assert_equal 0, exits
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_popping_the_last_modal_fires_on_exit_not_on_focus_change
|
|
132
|
+
exits = 0
|
|
133
|
+
focus_changes = []
|
|
134
|
+
settings = FakeWindow.new
|
|
135
|
+
stack = Teek::UI::ModalStack.new(
|
|
136
|
+
on_enter: ->(_) { },
|
|
137
|
+
on_exit: -> { exits += 1 },
|
|
138
|
+
on_focus_change: ->(name) { focus_changes << name },
|
|
139
|
+
)
|
|
140
|
+
stack.push(:settings, settings)
|
|
141
|
+
focus_changes.clear
|
|
142
|
+
|
|
143
|
+
stack.pop
|
|
144
|
+
|
|
145
|
+
refute stack.active?
|
|
146
|
+
assert_nil stack.current
|
|
147
|
+
assert_equal 0, stack.size
|
|
148
|
+
assert_equal [true], settings.hides
|
|
149
|
+
assert_equal 1, exits
|
|
150
|
+
assert_equal [], focus_changes
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_pop_on_an_empty_stack_is_a_safe_no_op
|
|
154
|
+
exits = 0
|
|
155
|
+
stack = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { exits += 1 })
|
|
156
|
+
|
|
157
|
+
stack.pop
|
|
158
|
+
|
|
159
|
+
refute stack.active?
|
|
160
|
+
assert_equal 0, exits
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_on_focus_change_is_optional
|
|
164
|
+
settings = FakeWindow.new
|
|
165
|
+
stack = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { })
|
|
166
|
+
|
|
167
|
+
stack.push(:settings, settings)
|
|
168
|
+
stack.pop
|
|
169
|
+
|
|
170
|
+
assert_equal [true], settings.shows
|
|
171
|
+
assert_equal [true], settings.hides
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_document_is_forwarded_to_the_internal_screens_for_lazy_realize
|
|
175
|
+
settings = FakeLazyWindow.new
|
|
176
|
+
stack = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { }, document: :the_document)
|
|
177
|
+
|
|
178
|
+
stack.push(:settings, settings)
|
|
179
|
+
|
|
180
|
+
assert_equal [:the_document], settings.realize_calls
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_pop_returns_the_popped_window
|
|
184
|
+
settings = FakeWindow.new
|
|
185
|
+
stack = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { })
|
|
186
|
+
stack.push(:settings, settings)
|
|
187
|
+
|
|
188
|
+
assert_same settings, stack.pop
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_pop_on_an_empty_stack_returns_nil
|
|
192
|
+
stack = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { })
|
|
193
|
+
|
|
194
|
+
assert_nil stack.pop
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_a_push_push_pop_pop_sequence_fires_on_enter_once_on_focus_change_thrice_on_exit_once
|
|
198
|
+
enters = []
|
|
199
|
+
exits = 0
|
|
200
|
+
focus_changes = []
|
|
201
|
+
settings = FakeWindow.new
|
|
202
|
+
replay = FakeWindow.new
|
|
203
|
+
stack = Teek::UI::ModalStack.new(
|
|
204
|
+
on_enter: ->(name) { enters << name },
|
|
205
|
+
on_exit: -> { exits += 1 },
|
|
206
|
+
on_focus_change: ->(name) { focus_changes << name },
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
stack.push(:settings, settings)
|
|
210
|
+
stack.push(:replay, replay)
|
|
211
|
+
stack.pop
|
|
212
|
+
stack.pop
|
|
213
|
+
|
|
214
|
+
assert_equal [:settings], enters
|
|
215
|
+
assert_equal [:settings, :replay, :settings], focus_changes
|
|
216
|
+
assert_equal 1, exits
|
|
217
|
+
refute stack.active?
|
|
218
|
+
end
|
|
219
|
+
end
|