teek-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +452 -0
  4. data/lib/teek/ui/canvas_item.rb +270 -0
  5. data/lib/teek/ui/component_handle.rb +32 -0
  6. data/lib/teek/ui/document.rb +185 -0
  7. data/lib/teek/ui/errors.rb +41 -0
  8. data/lib/teek/ui/event_binding.rb +22 -0
  9. data/lib/teek/ui/event_bus.rb +53 -0
  10. data/lib/teek/ui/grid_validator.rb +89 -0
  11. data/lib/teek/ui/handle.rb +585 -0
  12. data/lib/teek/ui/image.rb +58 -0
  13. data/lib/teek/ui/keysyms.rb +72 -0
  14. data/lib/teek/ui/menu_builder.rb +130 -0
  15. data/lib/teek/ui/menu_entry_addressing.rb +62 -0
  16. data/lib/teek/ui/modal_stack.rb +99 -0
  17. data/lib/teek/ui/mouse_events.rb +31 -0
  18. data/lib/teek/ui/node.rb +141 -0
  19. data/lib/teek/ui/option_dump_parsing.rb +31 -0
  20. data/lib/teek/ui/overlay_anchors.rb +29 -0
  21. data/lib/teek/ui/overlay_validator.rb +33 -0
  22. data/lib/teek/ui/pane_validator.rb +30 -0
  23. data/lib/teek/ui/realized_node.rb +25 -0
  24. data/lib/teek/ui/realizer.rb +577 -0
  25. data/lib/teek/ui/scope.rb +40 -0
  26. data/lib/teek/ui/screens.rb +141 -0
  27. data/lib/teek/ui/session.rb +417 -0
  28. data/lib/teek/ui/tab_validator.rb +30 -0
  29. data/lib/teek/ui/text_content.rb +321 -0
  30. data/lib/teek/ui/tree_inspector.rb +97 -0
  31. data/lib/teek/ui/validator.rb +131 -0
  32. data/lib/teek/ui/var.rb +98 -0
  33. data/lib/teek/ui/version.rb +7 -0
  34. data/lib/teek/ui/widget_addressing.rb +47 -0
  35. data/lib/teek/ui/widget_dsl.rb +512 -0
  36. data/lib/teek/ui/widget_type.rb +242 -0
  37. data/lib/teek/ui/widget_types/button.rb +7 -0
  38. data/lib/teek/ui/widget_types/canvas.rb +15 -0
  39. data/lib/teek/ui/widget_types/checkbox.rb +7 -0
  40. data/lib/teek/ui/widget_types/column.rb +14 -0
  41. data/lib/teek/ui/widget_types/context_menu.rb +15 -0
  42. data/lib/teek/ui/widget_types/divider.rb +10 -0
  43. data/lib/teek/ui/widget_types/dropdown.rb +7 -0
  44. data/lib/teek/ui/widget_types/grid.rb +12 -0
  45. data/lib/teek/ui/widget_types/group.rb +7 -0
  46. data/lib/teek/ui/widget_types/label.rb +7 -0
  47. data/lib/teek/ui/widget_types/list.rb +7 -0
  48. data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
  49. data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
  50. data/lib/teek/ui/widget_types/menu_item.rb +20 -0
  51. data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
  52. data/lib/teek/ui/widget_types/number_box.rb +7 -0
  53. data/lib/teek/ui/widget_types/pane.rb +37 -0
  54. data/lib/teek/ui/widget_types/panel.rb +7 -0
  55. data/lib/teek/ui/widget_types/progress.rb +7 -0
  56. data/lib/teek/ui/widget_types/radio.rb +7 -0
  57. data/lib/teek/ui/widget_types/row.rb +14 -0
  58. data/lib/teek/ui/widget_types/scrollable.rb +17 -0
  59. data/lib/teek/ui/widget_types/slider.rb +7 -0
  60. data/lib/teek/ui/widget_types/spacer.rb +14 -0
  61. data/lib/teek/ui/widget_types/split.rb +17 -0
  62. data/lib/teek/ui/widget_types/tab.rb +35 -0
  63. data/lib/teek/ui/widget_types/table.rb +9 -0
  64. data/lib/teek/ui/widget_types/tabs.rb +10 -0
  65. data/lib/teek/ui/widget_types/text_area.rb +7 -0
  66. data/lib/teek/ui/widget_types/text_box.rb +7 -0
  67. data/lib/teek/ui/widget_types/tree.rb +9 -0
  68. data/lib/teek/ui/widget_types/window.rb +50 -0
  69. data/lib/teek/ui/widget_types.rb +121 -0
  70. data/lib/teek/ui/widget_validators.rb +62 -0
  71. data/lib/teek/ui.rb +54 -0
  72. data/teek-ui.gemspec +24 -0
  73. data/test/support/fake_app.rb +57 -0
  74. data/test/test_busy.rb +82 -0
  75. data/test/test_canvas_items.rb +551 -0
  76. data/test/test_clipboard.rb +87 -0
  77. data/test/test_close_handling.rb +118 -0
  78. data/test/test_component.rb +151 -0
  79. data/test/test_component_realtk.rb +120 -0
  80. data/test/test_debug_info.rb +158 -0
  81. data/test/test_document.rb +317 -0
  82. data/test/test_event_bus.rb +163 -0
  83. data/test/test_events.rb +164 -0
  84. data/test/test_fake_app_contract.rb +62 -0
  85. data/test/test_grid.rb +97 -0
  86. data/test/test_handle.rb +386 -0
  87. data/test/test_handle_destroy_realtk.rb +344 -0
  88. data/test/test_helper.rb +20 -0
  89. data/test/test_image.rb +24 -0
  90. data/test/test_image_realtk.rb +146 -0
  91. data/test/test_incremental_realize.rb +203 -0
  92. data/test/test_keysyms.rb +76 -0
  93. data/test/test_layout.rb +185 -0
  94. data/test/test_lazy_realize.rb +128 -0
  95. data/test/test_managed_window.rb +304 -0
  96. data/test/test_menu_dsl.rb +230 -0
  97. data/test/test_menu_entry_addressing.rb +73 -0
  98. data/test/test_menu_realize.rb +296 -0
  99. data/test/test_modal_handle.rb +147 -0
  100. data/test/test_modal_stack.rb +219 -0
  101. data/test/test_modal_stack_realtk.rb +131 -0
  102. data/test/test_native_scrollable.rb +250 -0
  103. data/test/test_node.rb +193 -0
  104. data/test/test_overlay.rb +139 -0
  105. data/test/test_raw.rb +58 -0
  106. data/test/test_reactive_vars.rb +118 -0
  107. data/test/test_realizer.rb +264 -0
  108. data/test/test_scope.rb +38 -0
  109. data/test/test_screens.rb +260 -0
  110. data/test/test_screens_realtk.rb +314 -0
  111. data/test/test_scrollable.rb +185 -0
  112. data/test/test_scrollable_wheel.rb +172 -0
  113. data/test/test_scrollbar_auto_hide.rb +127 -0
  114. data/test/test_split.rb +145 -0
  115. data/test/test_tabs.rb +201 -0
  116. data/test/test_text_content.rb +664 -0
  117. data/test/test_toast.rb +126 -0
  118. data/test/test_tree_inspector.rb +183 -0
  119. data/test/test_ui.rb +428 -0
  120. data/test/test_validator.rb +209 -0
  121. data/test/test_var.rb +32 -0
  122. data/test/test_widget_dsl.rb +621 -0
  123. data/test/test_widget_introspection.rb +126 -0
  124. data/test/test_widget_types.rb +429 -0
  125. data/test/test_widget_types_realtk.rb +82 -0
  126. data/test/test_widget_validators.rb +82 -0
  127. metadata +207 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # ui.overlay floats a single widget on top of the enclosing ui.canvas at a
7
+ # fixed corner/edge/center anchor, via Tk's `place` geometry manager - the
8
+ # one legitimate "absolute position" case (a status readout or button bar
9
+ # layered over canvas content), not a general-purpose layout mode.
10
+ class TestOverlay < Minitest::Test
11
+ include TeekTestHelper
12
+
13
+ def test_overlay_places_the_widget_at_the_right_anchor
14
+ assert_tk_app("overlay should place its widget in the canvas at the anchor's relx/rely/anchor") do
15
+ require 'teek/ui'
16
+
17
+ session = Teek::UI.app(title: 'Overlay Test') do |ui|
18
+ ui.canvas(:board, width: 300, height: 200) do |cv|
19
+ cv.overlay(at: :bottom_right) { ui.label(:status, text: 'Ready') }
20
+ end
21
+ end
22
+ session.run_async
23
+ session.app.update
24
+
25
+ info = session.app.tcl_eval("place info #{session[:status].path}")
26
+ assert_match(/-in #{Regexp.escape(session[:board].path)}(?:\s|$)/, info)
27
+ assert_match(/-relx 1(?:\s|$)/, info)
28
+ assert_match(/-rely 1(?:\s|$)/, info)
29
+ assert_match(/-anchor se(?:\s|$)/, info)
30
+
31
+ session.app.destroy
32
+ end
33
+ end
34
+
35
+ def test_two_overlays_position_independently
36
+ assert_tk_app("two overlays on the same canvas should each land at their own anchor") do
37
+ require 'teek/ui'
38
+
39
+ session = Teek::UI.app(title: 'Overlay Test') do |ui|
40
+ ui.canvas(:board, width: 300, height: 200) do |cv|
41
+ cv.overlay(at: :top_left) { ui.label(:status, text: 'Ready') }
42
+ cv.overlay(at: :top_right) { ui.row(:controls) { ui.button(:pause, text: 'Pause') } }
43
+ end
44
+ end
45
+ session.run_async
46
+ session.app.update
47
+
48
+ status_info = session.app.tcl_eval("place info #{session[:status].path}")
49
+ controls_info = session.app.tcl_eval("place info #{session[:controls].path}")
50
+
51
+ assert_match(/-relx 0(?:\s|$)/, status_info)
52
+ assert_match(/-rely 0(?:\s|$)/, status_info)
53
+ assert_match(/-anchor nw(?:\s|$)/, status_info)
54
+
55
+ assert_match(/-relx 1(?:\s|$)/, controls_info)
56
+ assert_match(/-rely 0(?:\s|$)/, controls_info)
57
+ assert_match(/-anchor ne(?:\s|$)/, controls_info)
58
+
59
+ session.app.destroy
60
+ end
61
+ end
62
+
63
+ def test_overlay_tracks_a_canvas_resize
64
+ assert_tk_app("an overlay's real on-screen position should scale with the canvas, not stay fixed") do
65
+ require 'teek/ui'
66
+
67
+ session = Teek::UI.app(title: 'Overlay Test') do |ui|
68
+ ui.canvas(:board, width: 200, height: 200) do |cv|
69
+ cv.overlay(at: :bottom_right) { ui.label(:status, text: 'Ready') }
70
+ end
71
+ end
72
+ session.run_async
73
+ session.app.update
74
+
75
+ before_x = session.app.tcl_eval("winfo x #{session[:status].path}").to_i
76
+ before_y = session.app.tcl_eval("winfo y #{session[:status].path}").to_i
77
+
78
+ session.app.tcl_eval("#{session[:board].path} configure -width 500 -height 500")
79
+ session.app.update
80
+
81
+ after_x = session.app.tcl_eval("winfo x #{session[:status].path}").to_i
82
+ after_y = session.app.tcl_eval("winfo y #{session[:status].path}").to_i
83
+
84
+ assert_operator after_x, :>, before_x, "the overlay should have followed the canvas's new bottom-right corner"
85
+ assert_operator after_y, :>, before_y, "the overlay should have followed the canvas's new bottom-right corner"
86
+
87
+ session.app.destroy
88
+ end
89
+ end
90
+
91
+ def test_overlay_outside_a_canvas_raises
92
+ assert_tk_app("overlay declared outside ui.canvas should raise immediately, not silently do nothing") do
93
+ require 'teek/ui'
94
+
95
+ error = assert_raises(ArgumentError) do
96
+ Teek::UI.app(title: 'Overlay Test') { |ui| ui.overlay(at: :top_left) { ui.label(text: 'oops') } }
97
+ end
98
+ assert_match(/ui\.canvas/i, error.message)
99
+ end
100
+ end
101
+
102
+ def test_overlay_with_an_invalid_anchor_raises
103
+ assert_tk_app("overlay given an unrecognized at: should raise, not silently no-op") do
104
+ require 'teek/ui'
105
+
106
+ error = assert_raises(ArgumentError) do
107
+ Teek::UI.app(title: 'Overlay Test') do |ui|
108
+ ui.canvas(:board, width: 200, height: 200) { |cv| cv.overlay(at: :middle) { ui.label(text: 'oops') } }
109
+ end
110
+ end
111
+ assert_match(/at:/, error.message)
112
+ end
113
+ end
114
+
115
+ def test_overlay_requiring_exactly_one_widget_raises_on_zero_or_many
116
+ assert_tk_app("overlay's block must build exactly one widget") do
117
+ require 'teek/ui'
118
+
119
+ empty_error = assert_raises(ArgumentError) do
120
+ Teek::UI.app(title: 'Overlay Test') do |ui|
121
+ ui.canvas(:board, width: 200, height: 200) { |cv| cv.overlay(at: :top_left) { } }
122
+ end
123
+ end
124
+ assert_match(/exactly one/i, empty_error.message)
125
+
126
+ many_error = assert_raises(ArgumentError) do
127
+ Teek::UI.app(title: 'Overlay Test') do |ui|
128
+ ui.canvas(:board, width: 200, height: 200) do |cv|
129
+ cv.overlay(at: :top_left) do
130
+ ui.label(text: 'one')
131
+ ui.label(text: 'two')
132
+ end
133
+ end
134
+ end
135
+ end
136
+ assert_match(/exactly one/i, many_error.message)
137
+ end
138
+ end
139
+ end
data/test/test_raw.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ class TestRaw < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ def test_raw_runs_at_realize_with_a_working_live_app
10
+ assert_tk_app("ui.raw's block should run at realize, with a real, usable app") do
11
+ require 'teek/ui'
12
+
13
+ session = Teek::UI.app(title: 'Raw Test') do |ui|
14
+ ui.raw { |app| app.command('ttk::label', '.raw_label', text: 'from raw') }
15
+ end
16
+ session.run_async
17
+ session.app.update
18
+
19
+ assert session.app.winfo.exists?('.raw_label')
20
+ assert_equal 'from raw', session.app.command('.raw_label', :cget, '-text')
21
+
22
+ session.app.destroy
23
+ end
24
+ end
25
+
26
+ def test_raw_does_not_run_before_realize
27
+ assert_tk_app("ui.raw's block should not fire during build, only at realize") do
28
+ require 'teek/ui'
29
+
30
+ executed = false
31
+ session = Teek::UI.app(title: 'Raw Test') { |ui| ui.raw { |_app| executed = true } }
32
+
33
+ refute executed, "raw block ran during build, before any interpreter existed"
34
+
35
+ session.realize
36
+ assert executed, "raw block did not run at realize"
37
+
38
+ session.app.destroy
39
+ end
40
+ end
41
+
42
+ def test_raw_op_resolves_a_forward_referenced_sibling_by_name
43
+ assert_tk_app("a raw op declared before a sibling it references should still resolve its realized path") do
44
+ require 'teek/ui'
45
+
46
+ session = Teek::UI.app(title: 'Raw Test') do |ui|
47
+ ui.raw { |app| app.command(ui[:later].path, :configure, text: 'Changed by raw') }
48
+ ui.button(:later, text: 'Original')
49
+ end
50
+ session.run_async
51
+ session.app.update
52
+
53
+ assert_equal 'Changed by raw', session.app.command(session[:later].path, :cget, '-text')
54
+
55
+ session.app.destroy
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ class TestReactiveVars < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ def test_slider_and_label_stay_in_sync_bidirectionally
10
+ assert_tk_app("a var bound to a slider and a label should keep both in sync") do
11
+ require 'teek/ui'
12
+
13
+ speed = nil
14
+ session = Teek::UI.app(title: 'Reactive Vars Test') do |ui|
15
+ speed = ui.var(5)
16
+ ui.slider(:speed_slider, from: 1, to: 10, bind: speed)
17
+ ui.label(:speed_label, bind: speed)
18
+ end
19
+ session.run_async
20
+ session.app.update
21
+
22
+ assert_equal 5, speed.value
23
+
24
+ slider_path = session[:speed_slider].path
25
+ label_path = session[:speed_label].path
26
+
27
+ # slider -> var -> label
28
+ # ttk::scale always stores/formats its bound variable as a float
29
+ # (e.g. "7.0"), even for a whole-number -from/-to range - compare
30
+ # numerically rather than assuming a particular string format.
31
+ session.app.command(slider_path, :set, 7)
32
+ session.app.update
33
+ assert_equal 7, speed.value
34
+ assert_equal 7.0, session.app.command(label_path, :cget, '-text').to_f
35
+
36
+ # var -> slider and label
37
+ speed.value = 3
38
+ session.app.update
39
+ assert_equal 3.0, session.app.command(slider_path, :get).to_f
40
+ assert_equal 3.0, session.app.command(label_path, :cget, '-text').to_f
41
+
42
+ session.app.destroy
43
+ end
44
+ end
45
+
46
+ def test_text_box_is_two_way
47
+ assert_tk_app("a var bound to a text_box should sync in both directions") do
48
+ require 'teek/ui'
49
+
50
+ name = nil
51
+ session = Teek::UI.app(title: 'Reactive Vars Test') { |ui| name = ui.var(''); ui.text_box(:name_box, bind: name) }
52
+ session.run_async
53
+ session.app.update
54
+
55
+ # var -> text_box
56
+ name.value = 'hello'
57
+ session.app.update
58
+ assert_equal 'hello', session.app.command(session[:name_box].path, :get)
59
+
60
+ # text_box -> var
61
+ box_path = session[:name_box].path
62
+ session.app.command(box_path, :delete, 0, :end)
63
+ session.app.command(box_path, :insert, 0, 'typed')
64
+ session.app.update
65
+ assert_equal 'typed', name.value
66
+
67
+ session.app.destroy
68
+ end
69
+ end
70
+
71
+ def test_on_change_fires_with_the_coerced_value_regardless_of_which_widget_changed_it
72
+ assert_tk_app("on_change should fire with a coerced Integer, triggered by the bound widget") do
73
+ require 'teek/ui'
74
+
75
+ speed = nil
76
+ changes = []
77
+ session = Teek::UI.app(title: 'Reactive Vars Test') do |ui|
78
+ speed = ui.var(5)
79
+ speed.on_change { |v| changes << v }
80
+ ui.slider(:speed_slider, from: 1, to: 10, bind: speed)
81
+ end
82
+ session.run_async
83
+ session.app.update
84
+
85
+ session.app.command(session[:speed_slider].path, :set, 9)
86
+ session.app.update
87
+
88
+ assert_includes changes, 9
89
+ assert_kind_of Integer, changes.last
90
+
91
+ session.app.destroy
92
+ end
93
+ end
94
+
95
+ def test_boolean_var_coerces_correctly_for_a_checkbox
96
+ assert_tk_app("a Boolean var bound to a checkbox should use Tk's 1/0 convention and coerce back to true/false") do
97
+ require 'teek/ui'
98
+
99
+ enabled = nil
100
+ session = Teek::UI.app(title: 'Reactive Vars Test') do |ui|
101
+ enabled = ui.var(true)
102
+ ui.checkbox(:enabled_box, bind: enabled)
103
+ end
104
+ session.run_async
105
+ session.app.update
106
+
107
+ assert_equal true, enabled.value
108
+ assert_equal '1', session.app.get_variable(enabled.name)
109
+
110
+ enabled.value = false
111
+ session.app.update
112
+ assert_equal false, enabled.value
113
+ assert_equal '0', session.app.get_variable(enabled.name)
114
+
115
+ session.app.destroy
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ class TestRealizer < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ def test_realize_creates_real_mapped_widgets_with_hierarchical_paths
10
+ assert_tk_app("realizing a nested tree should create real, mapped widgets at hierarchical paths") do
11
+ require 'teek/ui'
12
+
13
+ session = Teek::UI.app(title: 'Realizer Test') do |ui|
14
+ ui.panel(:controls) do |p|
15
+ p.button(:go, text: 'Go')
16
+ end
17
+ end
18
+ session.run_async
19
+ session.app.update
20
+
21
+ go_handle = session[:go]
22
+ assert_equal '.controls.go', go_handle.path
23
+ assert session.app.winfo.exists?(go_handle.path)
24
+ assert session.app.winfo.ismapped?(go_handle.path), "realized widgets should be packed/visible, not just created"
25
+
26
+ session.app.destroy
27
+ end
28
+ end
29
+
30
+ def test_unnamed_nodes_get_a_valid_auto_generated_path_segment
31
+ assert_tk_app("an unnamed node should still realize to a real, addressable Tk path") do
32
+ require 'teek/ui'
33
+
34
+ handle = nil
35
+ session = Teek::UI.app(title: 'Realizer Test') { |ui| handle = ui.label(text: 'Hi') }
36
+ session.run_async
37
+ session.app.update
38
+
39
+ assert session.app.winfo.exists?(handle.path)
40
+ # unnamed nodes get their Document-assigned key (e.g. "#anon1") as
41
+ # their path segment - unique for the whole document, not a
42
+ # per-Realizer-instance counter, so it stays collision-free across
43
+ # separate realize_subtree calls too (see Session#add).
44
+ assert_match(/\A\.\S+\z/, handle.path)
45
+
46
+ session.app.destroy
47
+ end
48
+ end
49
+
50
+ def test_handle_configure_mutates_the_live_widget_post_realize
51
+ assert_tk_app("handle.configure after realize should mutate the real widget") do
52
+ require 'teek/ui'
53
+
54
+ session = Teek::UI.app(title: 'Realizer Test') { |ui| ui.label(:greeting, text: 'Hi') }
55
+ session.run_async
56
+ session.app.update
57
+
58
+ session[:greeting].configure(text: 'Bye')
59
+
60
+ assert_equal 'Bye', session.app.command(session[:greeting].path, :cget, '-text')
61
+
62
+ session.app.destroy
63
+ end
64
+ end
65
+
66
+ def test_forward_event_target_reference_resolves_and_fires
67
+ assert_tk_app("an event binding targeting a widget declared later should resolve once the whole tree is realized") do
68
+ require 'teek/ui'
69
+
70
+ fired = false
71
+ session = Teek::UI.app(title: 'Realizer Test') do |ui|
72
+ ui.button(:trigger, text: 'Trigger')
73
+ ui.label(:downstream, text: 'Target') # declared AFTER :trigger
74
+
75
+ # white-box: no event DSL yet (that's a separate bead), so attach the
76
+ # binding directly the way the future DSL will - a forward reference
77
+ # by name to a node the realizer hasn't created yet at build time.
78
+ ui.document.find(:trigger).events <<
79
+ Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { fired = true }, target: :downstream)
80
+ end
81
+ session.run_async
82
+ session.app.update
83
+
84
+ downstream_path = session[:downstream].path
85
+ session.app.tcl_eval("event generate #{downstream_path} <Button-1>")
86
+ session.app.update
87
+
88
+ assert fired, "the forward-referenced target's binding did not fire"
89
+
90
+ session.app.destroy
91
+ end
92
+ end
93
+
94
+ def test_event_target_inside_a_component_resolves_to_that_components_own_node_not_a_siblings
95
+ assert_tk_app("a binding target inside a component should resolve to THAT component's like-named node, never a sibling component's") do
96
+ require 'teek/ui'
97
+
98
+ a_fired = false
99
+ b_fired = false
100
+ comp_a_downstream = nil
101
+ session = Teek::UI.app(title: 'Realizer Test') do |ui|
102
+ # each component nested under its own panel, matching how
103
+ # test_component_realtk.rb mounts like-named siblings - two
104
+ # components' children only get distinct real Tk paths when they
105
+ # sit under different real parents.
106
+ ui.panel(:panel_a) do |p|
107
+ p.component { |c| c.button(:trigger, text: 'Trigger A'); comp_a_downstream = c.label(:downstream, text: 'Target A') }
108
+ end
109
+ ui.panel(:panel_b) do |p|
110
+ p.component { |c| c.button(:trigger, text: 'Trigger B'); c.label(:downstream, text: 'Target B') }
111
+ end
112
+
113
+ # white-box: target: isn't public DSL yet (see the plain forward-
114
+ # reference test above) - a component's own nodes aren't reachable
115
+ # by name from outside its scope, so walk the tree structurally
116
+ # (each panel has one component's 2 children as its own children,
117
+ # in build order) to attach each component's own binding. A
118
+ # binding's event listener attaches to the TARGET node's path (see
119
+ # the plain forward-reference test above), so generating the event
120
+ # on component A's own :downstream widget below only fires
121
+ # component A's binding if resolution correctly stayed within
122
+ # component A's scope.
123
+ comp_a_trigger_node, comp_a_downstream_node = ui.document.find(:panel_a).children
124
+ comp_b_trigger_node, comp_b_downstream_node = ui.document.find(:panel_b).children
125
+ comp_a_trigger_node.events <<
126
+ Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { a_fired = true }, target: :downstream)
127
+ comp_b_trigger_node.events <<
128
+ Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { b_fired = true }, target: :downstream)
129
+ raise "unexpected build shape" unless comp_a_downstream_node && comp_b_downstream_node
130
+ end
131
+ session.run_async
132
+ session.app.update
133
+
134
+ session.app.tcl_eval("event generate #{comp_a_downstream.path} <Button-1>")
135
+ session.app.update
136
+
137
+ assert a_fired, "component A's own trigger should have fired component A's own binding"
138
+ refute b_fired, "component A's trigger firing should never fire component B's binding"
139
+
140
+ session.app.destroy
141
+ end
142
+ end
143
+
144
+ def test_event_target_forward_reference_still_resolves_inside_a_component
145
+ assert_tk_app("a forward reference to a not-yet-built sibling should still resolve when both are inside the same component") do
146
+ require 'teek/ui'
147
+
148
+ fired = false
149
+ session = Teek::UI.app(title: 'Realizer Test') do |ui|
150
+ ui.component do |c|
151
+ c.button(:trigger, text: 'Trigger')
152
+ c.label(:downstream, text: 'Target') # declared AFTER :trigger, same component
153
+
154
+ trigger_node, = c.document.root.children
155
+ trigger_node.events <<
156
+ Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { fired = true }, target: :downstream)
157
+ end
158
+ end
159
+ session.run_async
160
+ session.app.update
161
+
162
+ _trigger_node, downstream_node = session.document.root.children
163
+ session.app.tcl_eval("event generate #{downstream_node.realized.path} <Button-1>")
164
+ session.app.update
165
+
166
+ assert fired, "the intra-component forward-referenced target's binding did not fire"
167
+
168
+ session.app.destroy
169
+ end
170
+ end
171
+
172
+ def test_realize_is_atomic_on_a_mid_realize_error
173
+ assert_tk_app("an error partway through realize should leave the root window unmapped and the session not realized") do
174
+ require 'teek/ui'
175
+
176
+ session = Teek::UI.app(title: 'Realizer Test') do |ui|
177
+ ui.label(:first, text: 'Ok')
178
+ ui.document.root.add_child(Teek::UI::Node.new(type: :not_a_real_widget_type, name: :bad))
179
+ end
180
+
181
+ assert_raises(StandardError) { session.realize }
182
+
183
+ assert_raises(Teek::UI::NotRealizedError) { session.app }
184
+ end
185
+ end
186
+
187
+ # Locks the invariant the validator's "mixed pack+grid" note relies on
188
+ # instead of checking directly: every container realizes into its own
189
+ # dedicated Tk master, and arrange_children picks exactly one geometry
190
+ # manager per master, so no master should ever end up with a mix of
191
+ # pack- and grid-managed children. Verified against the REAL, realized
192
+ # Tk widget tree (winfo children/winfo manager), not just teek-ui's own
193
+ # Document structure.
194
+ def test_no_realized_master_ever_receives_more_than_one_geometry_manager
195
+ assert_tk_app("every realized master frame should be managed by exactly one geometry manager - representative nested tree: column > grid > row > panel") do
196
+ require 'teek/ui'
197
+
198
+ session = Teek::UI.app(title: 'Realizer Test') do |ui|
199
+ ui.column(:outer, gap: 4) do |c|
200
+ c.grid(:g, gap: 2) do |g|
201
+ g.cell(row: 0, col: 0) { g.label(:l1, text: 'A') }
202
+ g.cell(row: 0, col: 1) do
203
+ g.row(:r, gap: 2) do |r|
204
+ r.button(:b1, text: 'B')
205
+ r.panel(:p) { |p| p.button(:b2, text: 'C') }
206
+ end
207
+ end
208
+ end
209
+ c.button(:outer_btn, text: 'D')
210
+ end
211
+ end
212
+ session.run_async
213
+ session.app.update
214
+
215
+ # walks the REAL, realized Tk widget tree (winfo children/winfo
216
+ # manager) rather than teek-ui's own Document structure - this is
217
+ # what would actually misbehave if the invariant ever broke, wherever
218
+ # the break came from. A local recursive lambda, not a helper method -
219
+ # assert_tk_app re-evaluates this block's own source text in a
220
+ # separate worker context, which has no access to methods defined on
221
+ # the surrounding Minitest::Test class.
222
+ check = lambda do |path|
223
+ children = session.app.split_list(session.app.tcl_eval("winfo children #{path}"))
224
+ managers = children.map { |child| session.app.tcl_eval("winfo manager #{child}") }.reject(&:empty?).uniq
225
+ assert_operator managers.length, :<=, 1,
226
+ "#{path} has children managed by more than one geometry manager: #{managers.inspect}"
227
+ children.each { |child| check.call(child) }
228
+ end
229
+ check.call('.')
230
+
231
+ session.app.destroy
232
+ end
233
+ end
234
+
235
+ # A genuinely mixed master turns out to be impossible to even construct
236
+ # via pack/grid themselves - Tk's own geometry-manager framework refuses
237
+ # a second manager type for a master once one is active, symmetrically
238
+ # (pack-then-grid and grid-then-pack both raise), with a clear, immediate
239
+ # Teek::TclError - NOT a silent hang, contrary to the original "Tk
240
+ # literally HANGS on this" framing this project started from. Confirmed
241
+ # empirically - place coexists safely with either, confirming overlay
242
+ # (not built yet, place-based) is a genuinely different, safe case. This
243
+ # is a second, independent safety net on top of the realizer's own
244
+ # one-manager-per-master construction invariant the test above locks -
245
+ # not a replacement for it.
246
+ def test_tk_itself_refuses_a_second_geometry_manager_on_one_master
247
+ assert_tk_app("Tk's own geometry-manager protection should refuse pack+grid mixing on one master with a clear error") do
248
+ require 'teek/ui'
249
+
250
+ session = Teek::UI.app(title: 'Realizer Test') { |ui| ui.panel(:guarded) }
251
+ session.run_async
252
+ session.app.update
253
+
254
+ session.app.command('ttk::button', '.guarded.a', text: 'A')
255
+ session.app.command('ttk::button', '.guarded.b', text: 'B')
256
+ session.app.command(:pack, '.guarded.a')
257
+
258
+ error = assert_raises(Teek::TclError) { session.app.command(:grid, '.guarded.b', row: 0, column: 0) }
259
+ assert_match(/geometry manager/i, error.message)
260
+
261
+ session.app.destroy
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/scope'
5
+
6
+ class TestScope < Minitest::Test
7
+ def test_top_level_is_top_level
8
+ assert Teek::UI::Scope::TOP_LEVEL.top_level?
9
+ end
10
+
11
+ def test_a_fresh_scope_is_not_top_level
12
+ refute Teek::UI::Scope.new.top_level?
13
+ end
14
+
15
+ def test_two_scopes_with_the_same_label_are_never_the_same_scope
16
+ a = Teek::UI::Scope.new(:widget)
17
+ b = Teek::UI::Scope.new(:widget)
18
+
19
+ refute a.equal?(b)
20
+ end
21
+
22
+ def test_parent_defaults_to_nil
23
+ assert_nil Teek::UI::Scope.new.parent
24
+ end
25
+
26
+ def test_parent_is_settable
27
+ parent = Teek::UI::Scope.new
28
+ child = Teek::UI::Scope.new(:child, parent: parent)
29
+
30
+ assert_same parent, child.parent
31
+ end
32
+
33
+ def test_label_is_readable
34
+ scope = Teek::UI::Scope.new(:sidebar)
35
+
36
+ assert_equal :sidebar, scope.label
37
+ end
38
+ end