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
data/test/test_ui.rb
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
class TestUI < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_app_yields_and_returns_the_same_session
|
|
10
|
+
assert_tk_app("Teek::UI.app should yield a session and return that same session") do
|
|
11
|
+
require 'teek/ui'
|
|
12
|
+
|
|
13
|
+
yielded = nil
|
|
14
|
+
session = Teek::UI.app(title: 'UI Scaffold Test') { |ui| yielded = ui }
|
|
15
|
+
|
|
16
|
+
assert_same session, yielded, "the block should receive the same session .app returns"
|
|
17
|
+
assert_kind_of Teek::UI::Session, session
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_build_constructs_no_tcl_interpreter
|
|
22
|
+
assert_tk_app("building a session should not construct any Teek::App/Interp until realize") do
|
|
23
|
+
require 'teek/ui'
|
|
24
|
+
|
|
25
|
+
baseline = Teek::Interp.instance_count
|
|
26
|
+
Teek::UI.app(title: 'UI Scaffold Test') { |ui| ui.document }
|
|
27
|
+
|
|
28
|
+
assert_equal baseline, Teek::Interp.instance_count,
|
|
29
|
+
"Teek::UI.app should not construct an interpreter before #realize/#run/#run_async"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_session_exposes_a_document_before_realize
|
|
34
|
+
assert_tk_app("session.document should be a real, empty Document, buildable with no interpreter") do
|
|
35
|
+
require 'teek/ui'
|
|
36
|
+
|
|
37
|
+
session = Teek::UI.app(title: 'UI Scaffold Test')
|
|
38
|
+
|
|
39
|
+
assert_kind_of Teek::UI::Document, session.document
|
|
40
|
+
assert_equal [], session.document.root.children
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_session_app_raises_before_realize
|
|
45
|
+
assert_tk_app("session.app should raise a clear error before realize") do
|
|
46
|
+
require 'teek/ui'
|
|
47
|
+
|
|
48
|
+
session = Teek::UI.app(title: 'UI Scaffold Test')
|
|
49
|
+
|
|
50
|
+
error = assert_raises(Teek::UI::NotRealizedError) { session.app }
|
|
51
|
+
assert_match(/not realized/i, error.message)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_session_every_and_after_queue_before_realize_and_fire_once_realized
|
|
56
|
+
assert_tk_app("session.every/.after declared inside the build block should queue (no NotRealizedError) and actually fire once realized") do
|
|
57
|
+
require 'teek/ui'
|
|
58
|
+
|
|
59
|
+
ticks = 0
|
|
60
|
+
fired = false
|
|
61
|
+
session = Teek::UI.app(title: 'Timers Test') do |ui|
|
|
62
|
+
ui.every(10) { ticks += 1 }
|
|
63
|
+
ui.after(10) { fired = true }
|
|
64
|
+
end
|
|
65
|
+
session.run_async
|
|
66
|
+
|
|
67
|
+
deadline = Time.now + 2
|
|
68
|
+
session.app.update until (ticks >= 2 && fired) || Time.now > deadline
|
|
69
|
+
|
|
70
|
+
assert_operator ticks, :>=, 2, "a build-block ui.every should have ticked after realize, same as a post-realize one"
|
|
71
|
+
assert fired, "a build-block ui.after should have fired after realize, same as a post-realize one"
|
|
72
|
+
|
|
73
|
+
session.app.destroy
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_a_build_block_timer_and_a_post_realize_timer_behave_identically
|
|
78
|
+
assert_tk_app("a timer declared inside the build block and one declared after realize should produce the same runtime behavior") do
|
|
79
|
+
require 'teek/ui'
|
|
80
|
+
|
|
81
|
+
queued_ticks = 0
|
|
82
|
+
session = Teek::UI.app(title: 'Timers Test') { |ui| ui.every(10) { queued_ticks += 1 } }
|
|
83
|
+
session.run_async
|
|
84
|
+
|
|
85
|
+
post_realize_ticks = 0
|
|
86
|
+
session.every(10) { post_realize_ticks += 1 }
|
|
87
|
+
|
|
88
|
+
deadline = Time.now + 2
|
|
89
|
+
session.app.update until (queued_ticks >= 2 && post_realize_ticks >= 2) || Time.now > deadline
|
|
90
|
+
|
|
91
|
+
assert_operator queued_ticks, :>=, 2
|
|
92
|
+
assert_operator post_realize_ticks, :>=, 2
|
|
93
|
+
|
|
94
|
+
session.app.destroy
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_session_every_returns_nil_when_queued_before_realize
|
|
99
|
+
assert_tk_app("session.every should return nil when queued (no live timer object exists yet to hand back)") do
|
|
100
|
+
require 'teek/ui'
|
|
101
|
+
|
|
102
|
+
result = nil
|
|
103
|
+
session = Teek::UI.app(title: 'Timers Test') { |ui| result = ui.every(10) { } }
|
|
104
|
+
session.run_async
|
|
105
|
+
session.app.update
|
|
106
|
+
|
|
107
|
+
assert_nil result
|
|
108
|
+
|
|
109
|
+
session.app.destroy
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_realize_creates_the_app_exactly_once
|
|
114
|
+
assert_tk_app("realize should create the app once and return the same app on repeat calls") do
|
|
115
|
+
require 'teek/ui'
|
|
116
|
+
|
|
117
|
+
session = Teek::UI.app(title: 'UI Scaffold Test')
|
|
118
|
+
baseline = Teek::Interp.instance_count
|
|
119
|
+
|
|
120
|
+
app1 = session.realize
|
|
121
|
+
app2 = session.realize
|
|
122
|
+
|
|
123
|
+
assert_same app1, app2, "realize should be idempotent, not build a second interpreter"
|
|
124
|
+
assert_equal baseline + 1, Teek::Interp.instance_count
|
|
125
|
+
|
|
126
|
+
session.app.destroy
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_session_app_after_realize_reflects_the_title
|
|
131
|
+
assert_tk_app("session.app after realize should expose the real Teek::App with the title applied") do
|
|
132
|
+
require 'teek/ui'
|
|
133
|
+
|
|
134
|
+
session = Teek::UI.app(title: 'UI Scaffold Test')
|
|
135
|
+
session.realize
|
|
136
|
+
|
|
137
|
+
assert_kind_of Teek::App, session.app
|
|
138
|
+
assert_equal 'UI Scaffold Test', session.app.wm.title(window: '.')
|
|
139
|
+
|
|
140
|
+
session.app.destroy
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_run_async_realizes_shows_the_window_and_returns_without_blocking
|
|
145
|
+
assert_tk_app("run_async should realize, show the window, and return the session without entering mainloop") do
|
|
146
|
+
require 'teek/ui'
|
|
147
|
+
|
|
148
|
+
session = Teek::UI.app(title: 'Run Async Test')
|
|
149
|
+
result = session.run_async
|
|
150
|
+
|
|
151
|
+
assert_same session, result
|
|
152
|
+
|
|
153
|
+
# run_async deliberately doesn't pump the event loop itself (that's the
|
|
154
|
+
# documented caveat) - the deiconify it issued only becomes visible to
|
|
155
|
+
# winfo after something processes events, same as a real caller would
|
|
156
|
+
# need to do between REPL statements.
|
|
157
|
+
session.app.update
|
|
158
|
+
assert session.app.winfo.ismapped?('.'), "run_async should have shown the root window"
|
|
159
|
+
|
|
160
|
+
session.app.destroy
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_every_delegates_to_the_underlying_app_after_realize
|
|
165
|
+
assert_tk_app("ui.every should delegate to App#every and actually tick, once realized") do
|
|
166
|
+
require 'teek/ui'
|
|
167
|
+
|
|
168
|
+
session = Teek::UI.app(title: 'Timers Test')
|
|
169
|
+
session.run_async
|
|
170
|
+
ticks = 0
|
|
171
|
+
timer = session.every(10) { ticks += 1 }
|
|
172
|
+
|
|
173
|
+
deadline = Time.now + 2
|
|
174
|
+
session.app.update until ticks >= 2 || Time.now > deadline
|
|
175
|
+
|
|
176
|
+
assert_operator ticks, :>=, 2, "ui.every's block did not tick"
|
|
177
|
+
timer.cancel
|
|
178
|
+
session.app.destroy
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_after_delegates_to_the_underlying_app_after_realize
|
|
183
|
+
assert_tk_app("ui.after should delegate to App#after, once realized") do
|
|
184
|
+
require 'teek/ui'
|
|
185
|
+
|
|
186
|
+
session = Teek::UI.app(title: 'Timers Test')
|
|
187
|
+
session.run_async
|
|
188
|
+
fired = false
|
|
189
|
+
session.after(10) { fired = true }
|
|
190
|
+
|
|
191
|
+
deadline = Time.now + 2
|
|
192
|
+
session.app.update until fired || Time.now > deadline
|
|
193
|
+
|
|
194
|
+
assert fired, "ui.after's block did not fire"
|
|
195
|
+
session.app.destroy
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_dialog_methods_raise_before_realize
|
|
200
|
+
assert_tk_app("session's dialog methods should raise a clear error before realize, not queue") do
|
|
201
|
+
require 'teek/ui'
|
|
202
|
+
|
|
203
|
+
session = Teek::UI.app(title: 'Dialogs Test')
|
|
204
|
+
|
|
205
|
+
assert_raises(Teek::UI::NotRealizedError) { session.open_file }
|
|
206
|
+
assert_raises(Teek::UI::NotRealizedError) { session.save_file }
|
|
207
|
+
assert_raises(Teek::UI::NotRealizedError) { session.message(message: 'Hi') }
|
|
208
|
+
assert_raises(Teek::UI::NotRealizedError) { session.choose_color }
|
|
209
|
+
assert_raises(Teek::UI::NotRealizedError) { session.choose_dir }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def test_open_file_forwards_every_option_under_its_real_tk_flag_name
|
|
214
|
+
assert_tk_app("session.open_file should forward every option to App#choose_open_file under the right flag") do
|
|
215
|
+
require 'teek/ui'
|
|
216
|
+
|
|
217
|
+
session = Teek::UI.app(title: 'Dialogs Test')
|
|
218
|
+
session.run_async
|
|
219
|
+
session.app.tcl_eval(<<~TCL)
|
|
220
|
+
proc tk_getOpenFile {args} {
|
|
221
|
+
set ::last_call $args
|
|
222
|
+
return {/tmp/picked.png}
|
|
223
|
+
}
|
|
224
|
+
TCL
|
|
225
|
+
|
|
226
|
+
result = session.open_file(
|
|
227
|
+
initialdir: '/tmp/open', initialfile: 'pick.png', title: 'Open It', multiple: true, parent: '.mywin'
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
assert_equal ['/tmp/picked.png'], result, "multiple: true should split Tk's result into an array"
|
|
231
|
+
captured = Hash[*session.app.split_list(session.app.tcl_eval('set ::last_call'))]
|
|
232
|
+
assert_equal(
|
|
233
|
+
{ '-initialdir' => '/tmp/open', '-initialfile' => 'pick.png', '-title' => 'Open It',
|
|
234
|
+
'-parent' => '.mywin', '-multiple' => '1' },
|
|
235
|
+
captured
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
session.app.destroy
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def test_save_file_forwards_every_option_under_its_real_tk_flag_name
|
|
243
|
+
assert_tk_app("session.save_file should forward every option to App#choose_save_file under the right flag") do
|
|
244
|
+
require 'teek/ui'
|
|
245
|
+
|
|
246
|
+
session = Teek::UI.app(title: 'Dialogs Test')
|
|
247
|
+
session.run_async
|
|
248
|
+
session.app.tcl_eval(<<~TCL)
|
|
249
|
+
proc tk_getSaveFile {args} {
|
|
250
|
+
set ::last_call $args
|
|
251
|
+
return {/tmp/out.png}
|
|
252
|
+
}
|
|
253
|
+
TCL
|
|
254
|
+
|
|
255
|
+
result = session.save_file(
|
|
256
|
+
initialdir: '/tmp/save', initialfile: 'out.png', title: 'Save It',
|
|
257
|
+
defaultextension: '.png', confirmoverwrite: false, parent: '.mywin'
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
assert_equal '/tmp/out.png', result
|
|
261
|
+
captured = Hash[*session.app.split_list(session.app.tcl_eval('set ::last_call'))]
|
|
262
|
+
assert_equal(
|
|
263
|
+
{ '-initialdir' => '/tmp/save', '-initialfile' => 'out.png', '-title' => 'Save It',
|
|
264
|
+
'-defaultextension' => '.png', '-confirmoverwrite' => '0', '-parent' => '.mywin' },
|
|
265
|
+
captured
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
session.app.destroy
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_message_forwards_every_option_under_its_real_tk_flag_name
|
|
273
|
+
assert_tk_app("session.message should forward every option to App#message_box under the right flag") do
|
|
274
|
+
require 'teek/ui'
|
|
275
|
+
|
|
276
|
+
session = Teek::UI.app(title: 'Dialogs Test')
|
|
277
|
+
session.run_async
|
|
278
|
+
session.app.tcl_eval(<<~TCL)
|
|
279
|
+
proc tk_messageBox {args} {
|
|
280
|
+
set ::last_call $args
|
|
281
|
+
return {yes}
|
|
282
|
+
}
|
|
283
|
+
TCL
|
|
284
|
+
|
|
285
|
+
result = session.message(
|
|
286
|
+
message: 'Sure?', title: 'Confirm', detail: 'Cannot be undone',
|
|
287
|
+
icon: :warning, type: :yesno, default: :no, parent: '.mywin'
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
assert_equal :yes, result
|
|
291
|
+
captured = Hash[*session.app.split_list(session.app.tcl_eval('set ::last_call'))]
|
|
292
|
+
assert_equal(
|
|
293
|
+
{ '-message' => 'Sure?', '-title' => 'Confirm', '-detail' => 'Cannot be undone',
|
|
294
|
+
'-icon' => 'warning', '-type' => 'yesno', '-default' => 'no', '-parent' => '.mywin' },
|
|
295
|
+
captured
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
session.app.destroy
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_choose_color_forwards_every_option_under_its_real_tk_flag_name
|
|
303
|
+
assert_tk_app("session.choose_color should forward every option to App#choose_color under the right flag") do
|
|
304
|
+
require 'teek/ui'
|
|
305
|
+
|
|
306
|
+
session = Teek::UI.app(title: 'Dialogs Test')
|
|
307
|
+
session.run_async
|
|
308
|
+
session.app.tcl_eval(<<~TCL)
|
|
309
|
+
proc tk_chooseColor {args} {
|
|
310
|
+
set ::last_call $args
|
|
311
|
+
return {#ff0080}
|
|
312
|
+
}
|
|
313
|
+
TCL
|
|
314
|
+
|
|
315
|
+
result = session.choose_color(initial: '#112233', title: 'Pick', parent: '.mywin')
|
|
316
|
+
|
|
317
|
+
assert_equal '#ff0080', result
|
|
318
|
+
captured = Hash[*session.app.split_list(session.app.tcl_eval('set ::last_call'))]
|
|
319
|
+
assert_equal({ '-initialcolor' => '#112233', '-title' => 'Pick', '-parent' => '.mywin' }, captured)
|
|
320
|
+
|
|
321
|
+
session.app.destroy
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def test_choose_dir_forwards_every_option_under_its_real_tk_flag_name
|
|
326
|
+
assert_tk_app("session.choose_dir should forward every option to App#choose_dir under the right flag") do
|
|
327
|
+
require 'teek/ui'
|
|
328
|
+
|
|
329
|
+
session = Teek::UI.app(title: 'Dialogs Test')
|
|
330
|
+
session.run_async
|
|
331
|
+
session.app.tcl_eval(<<~TCL)
|
|
332
|
+
proc tk_chooseDirectory {args} {
|
|
333
|
+
set ::last_call $args
|
|
334
|
+
return {/tmp/some dir}
|
|
335
|
+
}
|
|
336
|
+
TCL
|
|
337
|
+
|
|
338
|
+
result = session.choose_dir(initialdir: '/tmp/dir', mustexist: true, title: 'Folder', parent: '.mywin')
|
|
339
|
+
|
|
340
|
+
assert_equal '/tmp/some dir', result
|
|
341
|
+
captured = Hash[*session.app.split_list(session.app.tcl_eval('set ::last_call'))]
|
|
342
|
+
assert_equal(
|
|
343
|
+
{ '-initialdir' => '/tmp/dir', '-mustexist' => '1', '-title' => 'Folder', '-parent' => '.mywin' },
|
|
344
|
+
captured
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
session.app.destroy
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_realize_validates_before_constructing_any_interpreter
|
|
352
|
+
assert_tk_app("a validation failure should prevent any Teek::App/Interp from being constructed at all") do
|
|
353
|
+
require 'teek/ui'
|
|
354
|
+
|
|
355
|
+
baseline = Teek::Interp.instance_count
|
|
356
|
+
session = Teek::UI.app(title: 'Validation Test') do |ui|
|
|
357
|
+
ui.grid(:g) do |g|
|
|
358
|
+
g.cell(row: 0, col: 0) { g.label(:a, text: 'A') }
|
|
359
|
+
g.cell(row: 0, col: 0) { g.label(:b, text: 'B') }
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
error = assert_raises(Teek::UI::ValidationError) { session.realize }
|
|
364
|
+
assert_match(/row 0, col 0/, error.message)
|
|
365
|
+
|
|
366
|
+
assert_equal baseline, Teek::Interp.instance_count,
|
|
367
|
+
"a doomed build should never construct an interpreter, not even one that gets destroyed afterward"
|
|
368
|
+
assert_raises(Teek::UI::NotRealizedError) { session.app }
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def test_build_methods_raise_once_the_session_has_realized
|
|
373
|
+
assert_tk_app("ui.button/panel/raw/var/menu_bar/context_menu should all raise once the build has closed") do
|
|
374
|
+
require 'teek/ui'
|
|
375
|
+
|
|
376
|
+
session = Teek::UI.app(title: 'Closed Builder Test')
|
|
377
|
+
session.run_async
|
|
378
|
+
|
|
379
|
+
error = assert_raises(Teek::UI::ClosedBuilderError) { session.button(:late, text: 'Too late') }
|
|
380
|
+
assert_match(/session\.add/, error.message)
|
|
381
|
+
|
|
382
|
+
assert_raises(Teek::UI::ClosedBuilderError) { session.panel(:late) }
|
|
383
|
+
assert_raises(Teek::UI::ClosedBuilderError) { session.raw { } }
|
|
384
|
+
assert_raises(Teek::UI::ClosedBuilderError) { session.var(1) }
|
|
385
|
+
assert_raises(Teek::UI::ClosedBuilderError) { session.menu_bar }
|
|
386
|
+
assert_raises(Teek::UI::ClosedBuilderError) { session.context_menu }
|
|
387
|
+
|
|
388
|
+
session.app.destroy
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def test_build_methods_still_work_normally_before_realize
|
|
393
|
+
assert_tk_app("the closed-builder guard should never fire during the ordinary, still-open initial build") do
|
|
394
|
+
require 'teek/ui'
|
|
395
|
+
|
|
396
|
+
session = Teek::UI.app(title: 'Closed Builder Test') do |ui|
|
|
397
|
+
ui.button(:go, text: 'Go')
|
|
398
|
+
ui.var(1)
|
|
399
|
+
end
|
|
400
|
+
session.run_async
|
|
401
|
+
session.app.update
|
|
402
|
+
|
|
403
|
+
assert session.app.winfo.ismapped?(session[:go].path)
|
|
404
|
+
|
|
405
|
+
session.app.destroy
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def test_session_add_re_opens_the_builder_for_the_duration_of_its_own_block
|
|
410
|
+
assert_tk_app("session.add's own block should be exempt from the closed-builder guard") do
|
|
411
|
+
require 'teek/ui'
|
|
412
|
+
|
|
413
|
+
session = Teek::UI.app(title: 'Closed Builder Test') { |ui| ui.column(:list) }
|
|
414
|
+
session.run_async
|
|
415
|
+
|
|
416
|
+
session.add(:list) { |a| a.button(:added, text: 'Added') }
|
|
417
|
+
session.app.update
|
|
418
|
+
|
|
419
|
+
assert session.app.winfo.ismapped?(session[:added].path)
|
|
420
|
+
|
|
421
|
+
# the guard should still apply to code OUTSIDE session.add, even
|
|
422
|
+
# though it's fine again momentarily while add's own block runs
|
|
423
|
+
assert_raises(Teek::UI::ClosedBuilderError) { session.button(:still_too_late, text: 'Nope') }
|
|
424
|
+
|
|
425
|
+
session.app.destroy
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek/ui/session'
|
|
5
|
+
require 'teek/ui/validator'
|
|
6
|
+
|
|
7
|
+
class TestValidator < Minitest::Test
|
|
8
|
+
def build_session
|
|
9
|
+
Teek::UI::Session.new(title: 'Validator Test')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_a_clean_tree_passes_without_raising_or_warning
|
|
13
|
+
session = build_session
|
|
14
|
+
session.column(:controls) { |c| c.button(:go, text: 'Go') }
|
|
15
|
+
|
|
16
|
+
out, err = capture_io { Teek::UI::Validator.validate!(session.document) }
|
|
17
|
+
|
|
18
|
+
assert_empty out
|
|
19
|
+
assert_empty err
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_stray_cell_intent_under_a_non_grid_parent_raises
|
|
23
|
+
# only reachable via direct Node/Document manipulation - g.cell itself
|
|
24
|
+
# already refuses to run outside a ui.grid block, so the public DSL
|
|
25
|
+
# can never actually produce this.
|
|
26
|
+
document = Teek::UI::Document.new
|
|
27
|
+
panel = document.create(type: :panel, name: :not_a_grid)
|
|
28
|
+
document.root.add_child(panel)
|
|
29
|
+
stray = document.create(type: :label, name: :stray)
|
|
30
|
+
stray.layout = { cell: { row: 0, col: 0, span: 1 } }
|
|
31
|
+
panel.add_child(stray)
|
|
32
|
+
|
|
33
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(document) }
|
|
34
|
+
assert_match(/stray/, error.message)
|
|
35
|
+
assert_match(/not_a_grid/, error.message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_stray_overlay_intent_under_a_non_canvas_parent_raises
|
|
39
|
+
# only reachable via direct Node/Document manipulation - WidgetDSL#overlay
|
|
40
|
+
# itself already refuses to run outside a ui.canvas block.
|
|
41
|
+
document = Teek::UI::Document.new
|
|
42
|
+
panel = document.create(type: :panel, name: :not_a_canvas)
|
|
43
|
+
document.root.add_child(panel)
|
|
44
|
+
stray = document.create(type: :label, name: :stray)
|
|
45
|
+
stray.layout = { overlay: { at: :top_left } }
|
|
46
|
+
panel.add_child(stray)
|
|
47
|
+
|
|
48
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(document) }
|
|
49
|
+
assert_match(/overlay/, error.message)
|
|
50
|
+
assert_match(/not_a_canvas/, error.message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_a_tab_node_under_a_non_tabs_parent_raises
|
|
54
|
+
# only reachable via direct Node/Document manipulation - WidgetDSL#tab
|
|
55
|
+
# itself already refuses to run outside a ui.tabs block.
|
|
56
|
+
document = Teek::UI::Document.new
|
|
57
|
+
panel = document.create(type: :panel, name: :not_tabs)
|
|
58
|
+
document.root.add_child(panel)
|
|
59
|
+
stray = document.create(type: :tab, name: :stray, opts: { tab_label: 'Stray' })
|
|
60
|
+
panel.add_child(stray)
|
|
61
|
+
|
|
62
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(document) }
|
|
63
|
+
assert_match(/:tab/, error.message)
|
|
64
|
+
assert_match(/not_tabs/, error.message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_a_pane_node_under_a_non_split_parent_raises
|
|
68
|
+
# only reachable via direct Node/Document manipulation - WidgetDSL#pane
|
|
69
|
+
# itself already refuses to run outside a ui.split block.
|
|
70
|
+
document = Teek::UI::Document.new
|
|
71
|
+
panel = document.create(type: :panel, name: :not_split)
|
|
72
|
+
document.root.add_child(panel)
|
|
73
|
+
stray = document.create(type: :pane, name: :stray)
|
|
74
|
+
panel.add_child(stray)
|
|
75
|
+
|
|
76
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(document) }
|
|
77
|
+
assert_match(/:pane/, error.message)
|
|
78
|
+
assert_match(/not_split/, error.message)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_two_widgets_in_the_same_grid_cell_raises
|
|
82
|
+
session = build_session
|
|
83
|
+
session.grid(:g) do |g|
|
|
84
|
+
g.cell(row: 0, col: 0) { g.label(:a, text: 'A') }
|
|
85
|
+
g.cell(row: 0, col: 0) { g.label(:b, text: 'B') }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(session.document) }
|
|
89
|
+
assert_match(/row 0, col 0/, error.message)
|
|
90
|
+
assert_match(/:a\b/, error.message)
|
|
91
|
+
assert_match(/:b\b/, error.message)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_different_grid_cells_do_not_raise
|
|
95
|
+
session = build_session
|
|
96
|
+
session.grid(:g) do |g|
|
|
97
|
+
g.cell(row: 0, col: 0) { g.label(:a, text: 'A') }
|
|
98
|
+
g.cell(row: 0, col: 1) { g.label(:b, text: 'B') }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
capture_io { Teek::UI::Validator.validate!(session.document) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_a_grid_child_missing_a_cell_raises
|
|
105
|
+
# only reachable via direct Node/Document manipulation for a label
|
|
106
|
+
# specifically (the DSL's own ui.grid { |g| g.label(...) } would need
|
|
107
|
+
# g.cell to place it at all) - constructed directly here so the check
|
|
108
|
+
# is exercised in isolation from #test_a_widget_placed_directly_in_a_grid_without_cell_is_caught_by_validation's
|
|
109
|
+
# real-Tk, full-session version.
|
|
110
|
+
document = Teek::UI::Document.new
|
|
111
|
+
grid = document.create(type: :grid, name: :g)
|
|
112
|
+
document.root.add_child(grid)
|
|
113
|
+
oops = document.create(type: :label, name: :oops)
|
|
114
|
+
grid.add_child(oops)
|
|
115
|
+
|
|
116
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(document) }
|
|
117
|
+
assert_match(/cell/i, error.message)
|
|
118
|
+
assert_match(/oops/, error.message)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_raw_op_and_other_not_grid_arranged_types_inside_a_grid_do_not_need_a_cell
|
|
122
|
+
session = build_session
|
|
123
|
+
session.grid(:g) { |g| g.raw { |_app| } }
|
|
124
|
+
|
|
125
|
+
capture_io { Teek::UI::Validator.validate!(session.document) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_both_grid_misuse_directions_can_be_reported_together
|
|
129
|
+
document = Teek::UI::Document.new
|
|
130
|
+
grid = document.create(type: :grid, name: :g)
|
|
131
|
+
document.root.add_child(grid)
|
|
132
|
+
missing_cell = document.create(type: :label, name: :missing_cell)
|
|
133
|
+
grid.add_child(missing_cell)
|
|
134
|
+
panel = document.create(type: :panel, name: :not_a_grid)
|
|
135
|
+
document.root.add_child(panel)
|
|
136
|
+
stray = document.create(type: :label, name: :stray)
|
|
137
|
+
stray.layout = { cell: { row: 0, col: 0, span: 1 } }
|
|
138
|
+
panel.add_child(stray)
|
|
139
|
+
|
|
140
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(document) }
|
|
141
|
+
assert_match(/missing_cell/, error.message)
|
|
142
|
+
assert_match(/stray/, error.message)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_dangling_event_target_raises_naming_both_ends
|
|
146
|
+
# on_click et al never expose target: through the public Handle API
|
|
147
|
+
# (it's an internal mechanism for future forward-reference features),
|
|
148
|
+
# so this is only reachable via direct EventBinding construction too.
|
|
149
|
+
session = build_session
|
|
150
|
+
session.button(:trigger, text: 'Go')
|
|
151
|
+
session.document.find(:trigger).events <<
|
|
152
|
+
Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { }, target: :nope)
|
|
153
|
+
|
|
154
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(session.document) }
|
|
155
|
+
assert_match(/trigger/, error.message)
|
|
156
|
+
assert_match(/nope/, error.message)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_event_target_naming_a_different_components_node_is_reported_as_dangling_not_silently_matched
|
|
160
|
+
# a :downstream exists in the document - just not in the trigger's OWN
|
|
161
|
+
# scope, so flat resolution would silently find the WRONG one; scoped
|
|
162
|
+
# resolution should report this exactly like a genuinely nonexistent
|
|
163
|
+
# target, not silently match across the component boundary.
|
|
164
|
+
session = build_session
|
|
165
|
+
session.component { |c| c.label(:downstream, text: 'Elsewhere') }
|
|
166
|
+
session.component { |c| c.button(:trigger, text: 'Go') }
|
|
167
|
+
|
|
168
|
+
_elsewhere_downstream, trigger_node = session.document.root.children
|
|
169
|
+
trigger_node.events <<
|
|
170
|
+
Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { }, target: :downstream)
|
|
171
|
+
|
|
172
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(session.document) }
|
|
173
|
+
assert_match(/trigger/, error.message)
|
|
174
|
+
assert_match(/downstream/, error.message)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_orphan_named_node_warns_by_default
|
|
178
|
+
document = Teek::UI::Document.new
|
|
179
|
+
document.create(type: :button, name: :lost) # never attached to any parent
|
|
180
|
+
|
|
181
|
+
out, err = capture_io { Teek::UI::Validator.validate!(document) }
|
|
182
|
+
|
|
183
|
+
assert_empty out
|
|
184
|
+
assert_match(/lost/, err)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_orphan_named_node_raises_under_strict_mode
|
|
188
|
+
document = Teek::UI::Document.new
|
|
189
|
+
document.create(type: :button, name: :lost)
|
|
190
|
+
|
|
191
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(document, strict: true) }
|
|
192
|
+
assert_match(/lost/, error.message)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_multiple_problems_all_appear_in_one_raised_error
|
|
196
|
+
session = build_session
|
|
197
|
+
session.grid(:g) do |g|
|
|
198
|
+
g.cell(row: 0, col: 0) { g.label(:a, text: 'A') }
|
|
199
|
+
g.cell(row: 0, col: 0) { g.label(:b, text: 'B') }
|
|
200
|
+
end
|
|
201
|
+
session.button(:trigger, text: 'Go')
|
|
202
|
+
session.document.find(:trigger).events <<
|
|
203
|
+
Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { }, target: :nope)
|
|
204
|
+
|
|
205
|
+
error = assert_raises(Teek::UI::ValidationError) { Teek::UI::Validator.validate!(session.document) }
|
|
206
|
+
assert_match(/row 0, col 0/, error.message)
|
|
207
|
+
assert_match(/nope/, error.message)
|
|
208
|
+
end
|
|
209
|
+
end
|
data/test/test_var.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek/ui/var'
|
|
5
|
+
|
|
6
|
+
class TestVar < Minitest::Test
|
|
7
|
+
def test_name_is_the_allocated_tcl_variable_name
|
|
8
|
+
var = Teek::UI::Var.new('::teek_ui_var_1', 5)
|
|
9
|
+
|
|
10
|
+
assert_equal '::teek_ui_var_1', var.name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_value_raises_before_realize
|
|
14
|
+
var = Teek::UI::Var.new('::teek_ui_var_1', 5)
|
|
15
|
+
|
|
16
|
+
assert_raises(Teek::UI::NotRealizedError) { var.value }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_value_assignment_raises_before_realize
|
|
20
|
+
var = Teek::UI::Var.new('::teek_ui_var_1', 5)
|
|
21
|
+
|
|
22
|
+
assert_raises(Teek::UI::NotRealizedError) { var.value = 6 }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_on_change_is_queueable_before_realize_and_returns_self
|
|
26
|
+
var = Teek::UI::Var.new('::teek_ui_var_1', 5)
|
|
27
|
+
|
|
28
|
+
result = var.on_change { |v| v }
|
|
29
|
+
|
|
30
|
+
assert_same var, result
|
|
31
|
+
end
|
|
32
|
+
end
|