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
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