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,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Realize-phase introspection: Session#find_by_path (reverse path->node
7
+ # lookup) and Handle#options (a live options dump straight from Tk). Both
8
+ # need a real interpreter to mean anything - a raw Tk path only exists
9
+ # once something's actually realized, and #options parses Tk's own
10
+ # `configure` return value, so these live in their own suite rather than
11
+ # a headless one. Handle#events (the third piece of this same bead) is
12
+ # pure Ruby with no Tk involved and is covered in test_handle.rb instead.
13
+ class TestWidgetIntrospection < Minitest::Test
14
+ include TeekTestHelper
15
+
16
+ def test_find_by_path_raises_before_realize
17
+ assert_tk_app("find_by_path should raise before realize, matching every other realize-only diagnostic") do
18
+ require 'teek/ui'
19
+
20
+ session = Teek::UI.app(title: 'Widget Introspection Test')
21
+
22
+ assert_raises(Teek::UI::NotRealizedError) { session.find_by_path('.go') }
23
+ end
24
+ end
25
+
26
+ def test_find_by_path_returns_nil_for_an_unknown_path
27
+ assert_tk_app("find_by_path should return nil, not raise, for a path nothing was realized at") do
28
+ require 'teek/ui'
29
+
30
+ session = Teek::UI.app(title: 'Widget Introspection Test') { |ui| ui.button(:go, text: 'Go') }
31
+ session.run_async
32
+
33
+ assert_nil session.find_by_path('.nope')
34
+
35
+ session.app.destroy
36
+ end
37
+ end
38
+
39
+ def test_find_by_path_finds_the_handle_for_a_realized_widgets_real_path
40
+ assert_tk_app("find_by_path should resolve a real Tk path back to the same node ui[:name] would find") do
41
+ require 'teek/ui'
42
+
43
+ session = Teek::UI.app(title: 'Widget Introspection Test') { |ui| ui.button(:go, text: 'Go') }
44
+ session.run_async
45
+
46
+ found = session.find_by_path(session[:go].path)
47
+
48
+ refute_nil found
49
+ assert_equal :go, found.name
50
+ assert_equal :button, found.type
51
+
52
+ session.app.destroy
53
+ end
54
+ end
55
+
56
+ def test_find_by_path_resolves_a_widget_added_after_the_initial_realize
57
+ assert_tk_app("find_by_path should see a widget added dynamically via session.add, not just the initial build") do
58
+ require 'teek/ui'
59
+
60
+ session = Teek::UI.app(title: 'Widget Introspection Test') { |ui| ui.column(:list) }
61
+ session.run_async
62
+
63
+ session.add(:list) { |a| a.button(:item1, text: 'Item 1') }
64
+
65
+ found = session.find_by_path(session[:item1].path)
66
+
67
+ refute_nil found
68
+ assert_equal :item1, found.name
69
+
70
+ session.app.destroy
71
+ end
72
+ end
73
+
74
+ def test_options_raises_before_realize
75
+ assert_tk_app("options should raise before realize, matching #configure") do
76
+ require 'teek/ui'
77
+
78
+ session = Teek::UI.app(title: 'Widget Introspection Test') { |ui| ui.button(:go, text: 'Go') }
79
+
80
+ assert_raises(Teek::UI::NotRealizedError) { session[:go].options }
81
+ end
82
+ end
83
+
84
+ def test_options_reports_the_current_value_of_a_configured_option
85
+ assert_tk_app("options should reflect an option set at build time") do
86
+ require 'teek/ui'
87
+
88
+ session = Teek::UI.app(title: 'Widget Introspection Test') { |ui| ui.button(:go, text: 'Go') }
89
+ session.run_async
90
+
91
+ assert_equal 'Go', session[:go].options[:text]
92
+
93
+ session.app.destroy
94
+ end
95
+ end
96
+
97
+ def test_options_reflects_a_later_configure_call
98
+ assert_tk_app("options should reflect the CURRENT value, not just what the widget was built with") do
99
+ require 'teek/ui'
100
+
101
+ session = Teek::UI.app(title: 'Widget Introspection Test') { |ui| ui.button(:go, text: 'Go') }
102
+ session.run_async
103
+
104
+ session[:go].configure(text: 'Stop')
105
+
106
+ assert_equal 'Stop', session[:go].options[:text]
107
+
108
+ session.app.destroy
109
+ end
110
+ end
111
+
112
+ def test_options_works_on_a_menu_entry_which_has_no_real_tk_path_of_its_own
113
+ assert_tk_app("options should work through MenuEntryAddressing too, not just ordinary widgets") do
114
+ require 'teek/ui'
115
+
116
+ session = Teek::UI.app(title: 'Widget Introspection Test') do |ui|
117
+ ui.menu_bar { |mb| mb.menu(:file, label: 'File') { |f| f.item(:open, label: 'Open') { } } }
118
+ end
119
+ session.run_async
120
+
121
+ assert_equal 'Open', session[:open].options[:label]
122
+
123
+ session.app.destroy
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,429 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui'
5
+ require 'teek/ui/widget_types'
6
+ require 'teek/ui/widget_dsl'
7
+
8
+ class TestWidgetTypes < Minitest::Test
9
+ # type -> [tk_command, bind_option, natively_scrollable?] for every leaf
10
+ # widget type - a regression check independent of production code, not
11
+ # a copy of it.
12
+ LEAF_METADATA = {
13
+ text_box: ['ttk::entry', :textvariable, false],
14
+ text_area: ['text', nil, true],
15
+ label: ['ttk::label', :textvariable, false],
16
+ button: ['ttk::button', nil, false],
17
+ checkbox: ['ttk::checkbutton', :variable, false],
18
+ radio: ['ttk::radiobutton', nil, false],
19
+ slider: ['ttk::scale', :variable, false],
20
+ dropdown: ['ttk::combobox', :textvariable, false],
21
+ number_box: ['ttk::spinbox', :textvariable, false],
22
+ list: ['listbox', nil, true],
23
+ table: ['ttk::treeview', nil, true],
24
+ tree: ['ttk::treeview', nil, true],
25
+ progress: ['ttk::progressbar', :variable, false],
26
+ }.freeze
27
+
28
+ def test_every_leaf_widget_type_is_registered_with_the_right_metadata
29
+ LEAF_METADATA.each do |type, (tk_command, bind_option, natively_scrollable)|
30
+ widget_type = Teek::UI::WidgetTypes.for_type(type)
31
+
32
+ refute_nil widget_type, "expected :#{type} to be registered as a WidgetType"
33
+ assert widget_type.leaf?, ":#{type} should be a leaf"
34
+ assert_equal tk_command, widget_type.tk_command, ":#{type} tk_command"
35
+ bind_option.nil? ? assert_nil(widget_type.bind_option, ":#{type} bind_option") : assert_equal(bind_option, widget_type.bind_option, ":#{type} bind_option")
36
+ assert_equal natively_scrollable, widget_type.natively_scrollable?, ":#{type} natively_scrollable?"
37
+ end
38
+ end
39
+
40
+ def test_widget_dsl_carries_no_leaf_types_constant
41
+ refute Teek::UI::WidgetDSL.const_defined?(:LEAF_TYPES)
42
+ end
43
+
44
+ def test_widget_dsl_carries_no_bind_options_constant
45
+ refute Teek::UI::WidgetDSL.const_defined?(:BIND_OPTIONS)
46
+ end
47
+
48
+ def test_widget_dsl_carries_no_scrollable_types_constant
49
+ refute Teek::UI::WidgetDSL.const_defined?(:SCROLLABLE_TYPES)
50
+ end
51
+
52
+ def test_canvas_is_registered_as_natively_scrollable
53
+ widget_type = Teek::UI::WidgetTypes.for_type(:canvas)
54
+
55
+ refute_nil widget_type
56
+ assert widget_type.natively_scrollable?
57
+ end
58
+
59
+ def test_scroll_default_defaults_to_auto_scroll
60
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_scroll_default__, tk_command: 'ttk::label')
61
+
62
+ original = Teek::UI.auto_scroll
63
+ begin
64
+ Teek::UI.auto_scroll = :sentinel_value
65
+ assert_equal :sentinel_value, widget_type.global_scroll_default
66
+ ensure
67
+ Teek::UI.auto_scroll = original
68
+ end
69
+ end
70
+
71
+ def test_canvas_points_its_scroll_default_at_auto_scroll_canvas_not_auto_scroll
72
+ widget_type = Teek::UI::WidgetTypes.for_type(:canvas)
73
+
74
+ original = Teek::UI.auto_scroll_canvas
75
+ begin
76
+ Teek::UI.auto_scroll_canvas = :sentinel_value
77
+ assert_equal :sentinel_value, widget_type.global_scroll_default
78
+ ensure
79
+ Teek::UI.auto_scroll_canvas = original
80
+ end
81
+ end
82
+
83
+ def test_list_and_table_and_tree_and_text_area_use_the_shared_auto_scroll_default
84
+ %i[list table tree text_area].each do |type|
85
+ widget_type = Teek::UI::WidgetTypes.for_type(type)
86
+
87
+ original = Teek::UI.auto_scroll
88
+ begin
89
+ Teek::UI.auto_scroll = :sentinel_value
90
+ assert_equal :sentinel_value, widget_type.global_scroll_default, ":#{type} scroll_default"
91
+ ensure
92
+ Teek::UI.auto_scroll = original
93
+ end
94
+ end
95
+ end
96
+
97
+ def test_divider_is_registered_as_a_built_in
98
+ widget_type = Teek::UI::WidgetTypes.for_type(:divider)
99
+
100
+ refute_nil widget_type
101
+ assert_equal :divider, widget_type.type
102
+ assert_equal 'ttk::separator', widget_type.tk_command
103
+ end
104
+
105
+ def test_for_type_returns_nil_for_an_unregistered_type
106
+ assert_nil Teek::UI::WidgetTypes.for_type(:__never_registered__)
107
+ end
108
+
109
+ def test_for_type_accepts_a_string_type_too
110
+ assert_equal Teek::UI::WidgetTypes.for_type(:divider), Teek::UI::WidgetTypes.for_type('divider')
111
+ end
112
+
113
+ def test_register_raises_on_a_duplicate_type
114
+ Teek::UI::WidgetTypes.register(Teek::UI::WidgetType.new(type: :__test_widget_types_dup__, tk_command: 'ttk::label'))
115
+
116
+ error = assert_raises(ArgumentError) {
117
+ Teek::UI::WidgetTypes.register(Teek::UI::WidgetType.new(type: :__test_widget_types_dup__, tk_command: 'ttk::label'))
118
+ }
119
+ assert_match(/already registered/, error.message)
120
+ end
121
+
122
+ def test_each_enumerates_every_registered_type_including_divider
123
+ types = Teek::UI::WidgetTypes.each.map(&:type)
124
+
125
+ assert_includes types, :divider
126
+ end
127
+
128
+ def test_on_register_replays_every_already_registered_type
129
+ seen = []
130
+ Teek::UI::WidgetTypes.on_register { |widget_type| seen << widget_type.type }
131
+
132
+ assert_includes seen, :divider
133
+ end
134
+
135
+ def test_on_register_also_fires_for_a_type_registered_afterward
136
+ seen = []
137
+ Teek::UI::WidgetTypes.on_register { |widget_type| seen << widget_type.type }
138
+
139
+ Teek::UI::WidgetTypes.register(Teek::UI::WidgetType.new(type: :__test_widget_types_late__, tk_command: 'ttk::label'))
140
+
141
+ assert_includes seen, :__test_widget_types_late__
142
+ end
143
+
144
+ def test_register_forwards_a_composed_validator_into_widget_validators
145
+ validator = ->(_node, _parent, _document, errors) { errors << 'composed validator ran' }
146
+
147
+ Teek::UI::WidgetTypes.register(
148
+ Teek::UI::WidgetType.new(type: :__test_widget_types_validated__, tk_command: 'ttk::label', validator: validator)
149
+ )
150
+
151
+ dispatched = Teek::UI::WidgetValidators.for_type(:__test_widget_types_validated__)
152
+ errors = []
153
+ dispatched.each { |v| v.call(nil, nil, nil, errors) }
154
+
155
+ assert_equal ['composed validator ran'], errors
156
+ end
157
+
158
+ def test_a_type_with_no_validator_forwards_nothing
159
+ assert_empty Teek::UI::WidgetValidators.for_type(:divider)
160
+ end
161
+
162
+ def test_leaf_defaults
163
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_leaf_defaults__, tk_command: 'ttk::label')
164
+
165
+ assert widget_type.leaf?
166
+ refute widget_type.container?
167
+ refute widget_type.natively_scrollable?
168
+ assert_nil widget_type.bind_option
169
+ assert_nil widget_type.validator
170
+ end
171
+
172
+ def test_addressing_defaults_to_widget_addressing
173
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_addressing_default__, tk_command: 'ttk::label')
174
+
175
+ assert_equal Teek::UI::WidgetAddressing, widget_type.addressing
176
+ end
177
+
178
+ def test_addressing_is_registered_and_read_back
179
+ custom_strategy = Class.new
180
+ widget_type = Teek::UI::WidgetType.new(
181
+ type: :__test_widget_type_custom_addressing__, tk_command: 'ttk::label', addressing: custom_strategy
182
+ )
183
+
184
+ assert_equal custom_strategy, widget_type.addressing
185
+ end
186
+
187
+ def test_leaf_default_dsl_defines_a_method_that_calls_append_leaf
188
+ calls = []
189
+ fake_module = Class.new {
190
+ define_method(:append_leaf) { |type, name, opts| calls << [type, name, opts] }
191
+ }.new
192
+
193
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_dsl__, tk_command: 'ttk::label')
194
+ widget_type.define_dsl_method!(fake_module.singleton_class)
195
+ fake_module.__test_widget_type_dsl__(:thing, text: 'Hi')
196
+
197
+ assert_equal [[:__test_widget_type_dsl__, :thing, { text: 'Hi' }]], calls
198
+ end
199
+
200
+ def test_container_leaf_false_uses_append_container
201
+ calls = []
202
+ fake_module = Class.new {
203
+ define_method(:append_container) { |type, name, opts, &block| calls << [type, name, opts, block] }
204
+ }.new
205
+
206
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_container_dsl__, tk_command: 'ttk::frame', leaf: false)
207
+ widget_type.define_dsl_method!(fake_module.singleton_class)
208
+ fake_module.__test_widget_type_container_dsl__(:thing)
209
+
210
+ assert_equal :__test_widget_type_container_dsl__, calls.first[0]
211
+ assert_equal :thing, calls.first[1]
212
+ end
213
+
214
+ def test_post_create_defaults_to_a_no_op
215
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_no_post_create__, tk_command: 'ttk::label')
216
+
217
+ assert_nil widget_type.post_create(:app, :node, :path, :parent_path)
218
+ end
219
+
220
+ def test_post_create_runs_the_given_hook
221
+ calls = []
222
+ widget_type = Teek::UI::WidgetType.new(
223
+ type: :__test_widget_type_post_create__, tk_command: 'ttk::label',
224
+ post_create: ->(app, node, path, parent_path) { calls << [app, node, path, parent_path] }
225
+ )
226
+
227
+ widget_type.post_create(:app, :node, :path, :parent_path)
228
+
229
+ assert_equal [[:app, :node, :path, :parent_path]], calls
230
+ end
231
+
232
+ def test_flow_defaults_to_nil
233
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_no_flow__, tk_command: 'ttk::frame')
234
+
235
+ assert_nil widget_type.flow
236
+ end
237
+
238
+ def test_arranged_defaults_to_true
239
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_arranged_default__, tk_command: 'ttk::frame')
240
+
241
+ assert widget_type.arranged?
242
+ end
243
+
244
+ def test_arranged_false_is_registered_and_read_back
245
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_unarranged__, tk_command: 'toplevel', arranged: false)
246
+
247
+ refute widget_type.arranged?
248
+ end
249
+
250
+ def test_window_is_registered_as_unarranged
251
+ widget_type = Teek::UI::WidgetTypes.for_type(:window)
252
+
253
+ refute_nil widget_type
254
+ refute widget_type.arranged?
255
+ end
256
+
257
+ def test_column_and_row_are_registered_with_flow_config
258
+ column = Teek::UI::WidgetTypes.for_type(:column)
259
+ row = Teek::UI::WidgetTypes.for_type(:row)
260
+
261
+ refute_nil column.flow
262
+ refute_nil row.flow
263
+ assert_equal 'top', column.flow[:side]
264
+ assert_equal 'left', row.flow[:side]
265
+ end
266
+
267
+ # type -> [tk_command, leaf?, arranged?] for every special/branching
268
+ # container type (grid, scrollable, tabs/tab, split/pane, menu_bar/
269
+ # context_menu).
270
+ SPECIAL_TYPE_METADATA = {
271
+ grid: ['ttk::frame', false, true],
272
+ scrollable: ['ttk::frame', false, true],
273
+ tabs: ['ttk::notebook', false, true],
274
+ tab: ['ttk::frame', false, false],
275
+ split: ['ttk::panedwindow', false, true],
276
+ pane: ['ttk::frame', false, false],
277
+ menu_bar: ['menu', false, false],
278
+ context_menu: ['menu', false, false],
279
+ }.freeze
280
+
281
+ def test_every_special_type_is_registered_with_the_right_metadata
282
+ SPECIAL_TYPE_METADATA.each do |type, (tk_command, leaf, arranged)|
283
+ widget_type = Teek::UI::WidgetTypes.for_type(type)
284
+
285
+ refute_nil widget_type, "expected :#{type} to be registered as a WidgetType"
286
+ assert_equal tk_command, widget_type.tk_command, ":#{type} tk_command"
287
+ assert_equal leaf, widget_type.leaf?, ":#{type} leaf?"
288
+ assert_equal arranged, widget_type.arranged?, ":#{type} arranged?"
289
+ end
290
+ end
291
+
292
+ def test_tab_pane_split_menu_bar_context_menu_have_no_auto_generated_dsl_method
293
+ calls = []
294
+ fake_module = Class.new {
295
+ define_method(:append_container) { |*args, &block| calls << args }
296
+ define_method(:append_leaf) { |*args| calls << args }
297
+ }.new
298
+
299
+ %i[tab pane split menu_bar context_menu].each do |type|
300
+ Teek::UI::WidgetTypes.for_type(type).define_dsl_method!(fake_module.singleton_class)
301
+ end
302
+
303
+ refute fake_module.respond_to?(:tab)
304
+ refute fake_module.respond_to?(:pane)
305
+ refute fake_module.respond_to?(:split)
306
+ refute fake_module.respond_to?(:menu_bar)
307
+ refute fake_module.respond_to?(:context_menu)
308
+ assert_empty calls
309
+ end
310
+
311
+ def test_grid_scrollable_tabs_composed_validators_are_registered_exactly_once
312
+ assert_equal 1, Teek::UI::WidgetValidators.for_type(:grid).length
313
+ assert_equal 1, Teek::UI::WidgetValidators.for_type(:tab).length
314
+ assert_equal 1, Teek::UI::WidgetValidators.for_type(:pane).length
315
+ assert_empty Teek::UI::WidgetValidators.for_type(:scrollable)
316
+ assert_empty Teek::UI::WidgetValidators.for_type(:tabs)
317
+ end
318
+
319
+ def test_grid_has_a_custom_arrange_strategy
320
+ assert Teek::UI::WidgetTypes.for_type(:grid).arrange?
321
+ end
322
+
323
+ def test_scrollable_has_custom_children_and_arrange_strategies
324
+ widget_type = Teek::UI::WidgetTypes.for_type(:scrollable)
325
+
326
+ assert widget_type.custom_children?
327
+ assert widget_type.arrange?
328
+ end
329
+
330
+ def test_menu_bar_and_context_menu_have_a_custom_create_strategy
331
+ assert Teek::UI::WidgetTypes.for_type(:menu_bar).custom_create?
332
+ assert Teek::UI::WidgetTypes.for_type(:context_menu).custom_create?
333
+ end
334
+
335
+ def test_panel_has_none_of_the_special_strategies
336
+ widget_type = Teek::UI::WidgetTypes.for_type(:panel)
337
+
338
+ refute widget_type.arrange?
339
+ refute widget_type.custom_children?
340
+ refute widget_type.custom_create?
341
+ end
342
+
343
+ def test_arrange_defaults_to_absent
344
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_no_arrange__, tk_command: 'ttk::frame')
345
+
346
+ refute widget_type.arrange?
347
+ end
348
+
349
+ def test_arrange_runs_the_given_hook
350
+ calls = []
351
+ widget_type = Teek::UI::WidgetType.new(
352
+ type: :__test_widget_type_arrange__, tk_command: 'ttk::frame',
353
+ arrange: ->(realizer, node, children) { calls << [realizer, node, children] }
354
+ )
355
+
356
+ assert widget_type.arrange?
357
+ widget_type.arrange(:realizer, :node, :children)
358
+
359
+ assert_equal [[:realizer, :node, :children]], calls
360
+ end
361
+
362
+ def test_flow_computes_an_arrange_hook_that_delegates_to_arrange_flow
363
+ fake_realizer = Class.new {
364
+ attr_reader :calls
365
+ def initialize
366
+ @calls = []
367
+ end
368
+ def arrange_flow(node, children, flow)
369
+ @calls << [node, children, flow]
370
+ end
371
+ }.new
372
+
373
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_flow__, tk_command: 'ttk::frame', flow: { side: 'top' })
374
+
375
+ assert widget_type.arrange?
376
+ widget_type.arrange(fake_realizer, :node, :children)
377
+
378
+ assert_equal [[:node, :children, { side: 'top' }]], fake_realizer.calls
379
+ end
380
+
381
+ def test_custom_children_defaults_to_absent
382
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_no_custom_children__, tk_command: 'ttk::frame')
383
+
384
+ refute widget_type.custom_children?
385
+ end
386
+
387
+ def test_custom_children_runs_the_given_hook
388
+ calls = []
389
+ widget_type = Teek::UI::WidgetType.new(
390
+ type: :__test_widget_type_custom_children__, tk_command: 'ttk::frame',
391
+ custom_children: ->(realizer, node, path) { calls << [realizer, node, path] }
392
+ )
393
+
394
+ assert widget_type.custom_children?
395
+ widget_type.custom_children(:realizer, :node, :path)
396
+
397
+ assert_equal [[:realizer, :node, :path]], calls
398
+ end
399
+
400
+ def test_custom_create_defaults_to_absent
401
+ widget_type = Teek::UI::WidgetType.new(type: :__test_widget_type_no_custom_create__, tk_command: 'menu')
402
+
403
+ refute widget_type.custom_create?
404
+ end
405
+
406
+ def test_custom_create_runs_the_given_hook
407
+ calls = []
408
+ widget_type = Teek::UI::WidgetType.new(
409
+ type: :__test_widget_type_custom_create__, tk_command: 'menu',
410
+ custom_create: ->(realizer, node, parent_path) { calls << [realizer, node, parent_path] }
411
+ )
412
+
413
+ assert widget_type.custom_create?
414
+ widget_type.custom_create(:realizer, :node, :parent_path)
415
+
416
+ assert_equal [[:realizer, :node, :parent_path]], calls
417
+ end
418
+
419
+ def test_widget_dsl_carries_no_container_types_constant
420
+ refute Teek::UI::WidgetDSL.const_defined?(:CONTAINER_TYPES)
421
+ end
422
+
423
+ %i[TK_COMMANDS MENU_ROOT_TYPES NOT_ARRANGED_TYPES].each do |const_name|
424
+ define_method("test_realizer_carries_no_#{const_name.downcase}_constant") do
425
+ require 'teek/ui/realizer'
426
+ refute Teek::UI::Realizer.const_defined?(const_name)
427
+ end
428
+ end
429
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Real-Tk half of WidgetTypes' coverage - registry mechanics (register/
7
+ # for_type/each/on_register, leaf defaults, validator forwarding) are
8
+ # covered headlessly in test_widget_types.rb; these exercise real
9
+ # realize/validate behavior against a live app: `:divider` realizes as a
10
+ # genuine `ttk::separator`, and a brand-new custom type lights up its own
11
+ # `ui.<type>` DSL method, realize, and validator the moment it's
12
+ # registered - proving the registry is what actually drives WidgetDSL/
13
+ # Realizer/Validator, not a parallel mechanism alongside them.
14
+ class TestWidgetTypesRealTk < Minitest::Test
15
+ include TeekTestHelper
16
+
17
+ def test_divider_realizes_as_a_real_ttk_separator
18
+ assert_tk_app("ui.divider should realize as a real ttk::separator") do
19
+ require 'teek/ui'
20
+
21
+ session = Teek::UI.app(title: 'Widget Types Test') do |ui|
22
+ ui.divider(:sep)
23
+ end
24
+ session.run_async
25
+ session.app.update
26
+
27
+ assert_equal 'TSeparator', session.app.tcl_eval("winfo class #{session[:sep].path}")
28
+ assert session.app.winfo.ismapped?(session[:sep].path)
29
+
30
+ session.app.destroy
31
+ end
32
+ end
33
+
34
+ def test_a_custom_registered_widget_type_lights_up_dsl_realize_and_validate
35
+ assert_tk_app("registering a new WidgetType should light up ui.<type>, its realize, and its validator") do
36
+ require 'teek/ui'
37
+
38
+ custom_type = :__test_widget_type_end_to_end__
39
+ validated = []
40
+ validator = ->(node, parent, _document, _errors) { validated << [node.type, parent&.type] }
41
+
42
+ Teek::UI::WidgetTypes.register(
43
+ Teek::UI::WidgetType.new(type: custom_type, tk_command: 'ttk::label', validator: validator)
44
+ )
45
+
46
+ session = Teek::UI.app(title: 'Widget Types Test') do |ui|
47
+ ui.send(custom_type, :thing, text: 'Hi')
48
+ end
49
+ session.run_async
50
+ session.app.update
51
+
52
+ # realize: a real ttk::label got created, with the right opts
53
+ assert_equal 'Hi', session.app.command(session[:thing].path, :cget, '-text')
54
+
55
+ # validate: the composed validator ran during session.realize's
56
+ # internal Validator.validate! call, seeing the right node/parent types
57
+ assert_equal [[custom_type, :root]], validated
58
+
59
+ session.app.destroy
60
+ end
61
+ end
62
+
63
+ def test_a_custom_widget_type_without_a_validator_realizes_fine_and_is_never_dispatched
64
+ assert_tk_app("a descriptor with no validator: should realize normally and never appear in WidgetValidators") do
65
+ require 'teek/ui'
66
+
67
+ custom_type = :__test_widget_type_no_validator__
68
+ Teek::UI::WidgetTypes.register(
69
+ Teek::UI::WidgetType.new(type: custom_type, tk_command: 'ttk::label')
70
+ )
71
+
72
+ session = Teek::UI.app(title: 'Widget Types Test') { |ui| ui.send(custom_type, :thing, text: 'Hi') }
73
+ session.run_async
74
+ session.app.update
75
+
76
+ assert_equal 'Hi', session.app.command(session[:thing].path, :cget, '-text')
77
+ assert_empty Teek::UI::WidgetValidators.for_type(custom_type)
78
+
79
+ session.app.destroy
80
+ end
81
+ end
82
+ end