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,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Real-Tk half of ModalStack's coverage - the callback lifecycle (on_enter/
7
+ # on_exit/on_focus_change) is covered headlessly in test_modal_stack.rb;
8
+ # these exercise the actual show/hide/grab side effects against real
9
+ # ui.dialog windows.
10
+ class TestModalStackRealTk < Minitest::Test
11
+ include TeekTestHelper
12
+
13
+ def test_push_shows_and_grabs_the_dialog
14
+ assert_tk_app("pushing a modal dialog should map and grab it") do
15
+ require 'teek/ui'
16
+
17
+ session = Teek::UI.app(title: 'ModalStack Test') { |ui| ui.dialog(:settings) }
18
+ session.run_async
19
+ session.app.update
20
+
21
+ session.modal = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { })
22
+ session.modal.push(:settings, session[:settings])
23
+ session.app.update
24
+
25
+ path = session[:settings].path
26
+ assert session.app.winfo.ismapped?(path)
27
+ assert_equal path, session.app.tcl_eval("grab current #{path}")
28
+
29
+ session.app.destroy
30
+ end
31
+ end
32
+
33
+ def test_pushing_a_second_dialog_withdraws_the_first_and_grabs_the_second
34
+ assert_tk_app("pushing a second dialog should withdraw/release the first and grab the second") do
35
+ require 'teek/ui'
36
+
37
+ session = Teek::UI.app(title: 'ModalStack Test') do |ui|
38
+ ui.dialog(:settings)
39
+ ui.dialog(:replay)
40
+ end
41
+ session.run_async
42
+ session.app.update
43
+
44
+ session.modal = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { })
45
+ session.modal.push(:settings, session[:settings])
46
+ session.app.update
47
+ session.modal.push(:replay, session[:replay])
48
+ session.app.update
49
+
50
+ settings_path = session[:settings].path
51
+ replay_path = session[:replay].path
52
+
53
+ refute session.app.winfo.ismapped?(settings_path)
54
+ assert session.app.winfo.ismapped?(replay_path)
55
+ assert_equal replay_path, session.app.tcl_eval("grab current #{replay_path}")
56
+ assert_equal :replay, session.modal.current
57
+ assert_equal 2, session.modal.size
58
+
59
+ session.app.destroy
60
+ end
61
+ end
62
+
63
+ def test_pop_re_shows_and_re_grabs_the_dialog_underneath
64
+ assert_tk_app("popping the top dialog should re-show and re-grab the one underneath") do
65
+ require 'teek/ui'
66
+
67
+ session = Teek::UI.app(title: 'ModalStack Test') do |ui|
68
+ ui.dialog(:settings)
69
+ ui.dialog(:replay)
70
+ end
71
+ session.run_async
72
+ session.app.update
73
+
74
+ session.modal = Teek::UI::ModalStack.new(on_enter: ->(_) { }, on_exit: -> { })
75
+ session.modal.push(:settings, session[:settings])
76
+ session.modal.push(:replay, session[:replay])
77
+ session.app.update
78
+
79
+ session.modal.pop
80
+ session.app.update
81
+
82
+ settings_path = session[:settings].path
83
+ replay_path = session[:replay].path
84
+
85
+ assert session.app.winfo.ismapped?(settings_path)
86
+ refute session.app.winfo.ismapped?(replay_path)
87
+ assert_equal settings_path, session.app.tcl_eval("grab current #{settings_path}")
88
+ assert_equal :settings, session.modal.current
89
+
90
+ session.app.destroy
91
+ end
92
+ end
93
+
94
+ def test_the_on_enter_and_on_exit_callbacks_bracket_a_whole_modal_session
95
+ assert_tk_app("on_enter should fire on the first push, on_exit on the last pop") do
96
+ require 'teek/ui'
97
+
98
+ session = Teek::UI.app(title: 'ModalStack Test') do |ui|
99
+ ui.dialog(:settings)
100
+ ui.dialog(:replay)
101
+ end
102
+ session.run_async
103
+ session.app.update
104
+
105
+ events = []
106
+ session.modal = Teek::UI::ModalStack.new(
107
+ on_enter: ->(name) { events << [:enter, name] },
108
+ on_exit: -> { events << [:exit] },
109
+ on_focus_change: ->(name) { events << [:focus, name] },
110
+ )
111
+
112
+ session.modal.push(:settings, session[:settings])
113
+ session.modal.push(:replay, session[:replay])
114
+ session.app.update
115
+ session.modal.pop
116
+ session.modal.pop
117
+ session.app.update
118
+
119
+ assert_equal [
120
+ [:enter, :settings],
121
+ [:focus, :settings],
122
+ [:focus, :replay],
123
+ [:focus, :settings],
124
+ [:exit],
125
+ ], events
126
+ refute session.modal.active?
127
+
128
+ session.app.destroy
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # A bare list/text_area/table/tree/canvas auto-attaches a scrollbar
7
+ # wherever it's declared - no ui.scrollable wrapper needed. See
8
+ # Realizer#create_native_scrollable/#auto_scrollable?/#resolve_scroll.
9
+ #
10
+ # The widget is wrapped in an invisible frame (widget + scrollbar,
11
+ # gridded together) that takes over the node's own allocated path; the
12
+ # real widget lives one level deeper, at <path>.widget, which is what
13
+ # session[:name].path/.configure/events all still act on directly - see
14
+ # RealizedNode's arrange_path split. Validation that scroll: raises on an
15
+ # unsupported widget type is headless, in test_widget_dsl.rb.
16
+ class TestNativeScrollable < Minitest::Test
17
+ include TeekTestHelper
18
+
19
+ def test_a_bare_list_auto_attaches_a_vertical_scrollbar_by_default
20
+ assert_tk_app("ui.list should get a scrollbar with no ui.scrollable wrapper") do
21
+ require 'teek/ui'
22
+
23
+ session = Teek::UI.app(title: 'Native Scrollable Test') { |ui| ui.list(:items) }
24
+ session.run_async
25
+ session.app.update
26
+
27
+ list_path = session[:items].path
28
+ wrapper_path = list_path.sub(/\.widget\z/, '')
29
+ vsb_path = "#{wrapper_path}.vsb"
30
+
31
+ refute_equal wrapper_path, list_path, "the handle path should be the real widget, not the wrapper"
32
+ assert_equal '1', session.app.tcl_eval("winfo exists #{vsb_path}")
33
+ assert_equal '0', session.app.tcl_eval("winfo exists #{wrapper_path}.hsb"), "x: defaults to false"
34
+ assert_equal "#{list_path} yview", session.app.command(vsb_path, :cget, '-command')
35
+
36
+ session.app.destroy
37
+ end
38
+ end
39
+
40
+ def test_configure_and_events_act_on_the_real_widget_not_the_wrapper
41
+ assert_tk_app("Handle#configure/#on_click should still act on the real widget once auto-wrapped") do
42
+ require 'teek/ui'
43
+
44
+ session = Teek::UI.app(title: 'Native Scrollable Test') { |ui| ui.list(:items) }
45
+ session.run_async
46
+ session.app.update
47
+
48
+ clicked = false
49
+ session[:items].on_click { clicked = true }
50
+ session[:items].configure(exportselection: false)
51
+ session.app.update
52
+
53
+ assert_equal '0', session.app.command(session[:items].path, :cget, '-exportselection')
54
+
55
+ session.app.tcl_eval("event generate #{session[:items].path} <Button-1>")
56
+ session.app.update
57
+
58
+ assert clicked
59
+
60
+ session.app.destroy
61
+ end
62
+ end
63
+
64
+ def test_scroll_false_opts_out_leaving_a_plain_unwrapped_widget
65
+ assert_tk_app("scroll: false should leave the widget exactly as it was before auto-scroll existed") do
66
+ require 'teek/ui'
67
+
68
+ session = Teek::UI.app(title: 'Native Scrollable Test') do |ui|
69
+ ui.column(:panel) { |c| c.list(:items, scroll: false) }
70
+ end
71
+ session.run_async
72
+ session.app.update
73
+
74
+ assert_equal "#{session[:panel].path}.items", session[:items].path
75
+ assert_equal '0', session.app.tcl_eval("winfo exists #{session[:items].path}.vsb")
76
+
77
+ session.app.destroy
78
+ end
79
+ end
80
+
81
+ def test_text_area_and_table_and_tree_also_auto_attach
82
+ assert_tk_app("text_area/table/tree should get the same auto-attach treatment as list") do
83
+ require 'teek/ui'
84
+
85
+ session = Teek::UI.app(title: 'Native Scrollable Test') do |ui|
86
+ ui.text_area(:notes)
87
+ ui.table(:grid_view)
88
+ ui.tree(:hierarchy)
89
+ end
90
+ session.run_async
91
+ session.app.update
92
+
93
+ %i[notes grid_view hierarchy].each do |name|
94
+ wrapper = session[name].path.sub(/\.widget\z/, '')
95
+ assert_equal '1', session.app.tcl_eval("winfo exists #{wrapper}.vsb"), "#{name} should have auto-attached a scrollbar"
96
+ end
97
+
98
+ session.app.destroy
99
+ end
100
+ end
101
+
102
+ def test_canvas_defaults_to_no_auto_attach
103
+ assert_tk_app("canvas should default to scroll: false, unlike the other native types") do
104
+ require 'teek/ui'
105
+
106
+ session = Teek::UI.app(title: 'Native Scrollable Test') { |ui| ui.canvas(:board) }
107
+ session.run_async
108
+ session.app.update
109
+
110
+ assert_equal '.board', session[:board].path
111
+ assert_equal '0', session.app.tcl_eval("winfo exists .board.vsb")
112
+
113
+ session.app.destroy
114
+ end
115
+ end
116
+
117
+ def test_canvas_scroll_true_forces_it_on
118
+ assert_tk_app("scroll: true should override canvas's own false default") do
119
+ require 'teek/ui'
120
+
121
+ session = Teek::UI.app(title: 'Native Scrollable Test') { |ui| ui.canvas(:board, scroll: true) }
122
+ session.run_async
123
+ session.app.update
124
+
125
+ wrapper = session[:board].path.sub(/\.widget\z/, '')
126
+ assert_equal '1', session.app.tcl_eval("winfo exists #{wrapper}.vsb")
127
+
128
+ session.app.destroy
129
+ end
130
+ end
131
+
132
+ def test_canvas_children_still_create_inside_the_real_canvas_once_wrapped
133
+ assert_tk_app("a canvas's own DSL children (via ui.raw) should target the real canvas, not the wrapper") do
134
+ require 'teek/ui'
135
+
136
+ session = Teek::UI.app(title: 'Native Scrollable Test') do |ui|
137
+ # ui.canvas's own block yields the builder itself (not a scoped
138
+ # Handle - see WidgetDSL's own doc comment), so the real canvas
139
+ # handle is looked up by name via the outer `ui` instead.
140
+ ui.canvas(:board, scroll: true) { |c| c.raw { |app| app.command(ui[:board].path, :create, :text, 10, 10, text: 'hi') } }
141
+ end
142
+ session.run_async
143
+ session.app.update
144
+
145
+ item_ids = session.app.split_list(session.app.command(session[:board].path, :find, :all))
146
+ assert_equal 1, item_ids.length
147
+
148
+ session.app.destroy
149
+ end
150
+ end
151
+
152
+ def test_x_true_wires_a_horizontal_scrollbar_on_a_native_widget_too
153
+ assert_tk_app("x: true on a bare native widget should wire a horizontal scrollbar") do
154
+ require 'teek/ui'
155
+
156
+ session = Teek::UI.app(title: 'Native Scrollable Test') { |ui| ui.tree(:hierarchy, x: true) }
157
+ session.run_async
158
+ session.app.update
159
+
160
+ wrapper = session[:hierarchy].path.sub(/\.widget\z/, '')
161
+ assert_equal '1', session.app.tcl_eval("winfo exists #{wrapper}.hsb")
162
+ assert_equal "#{session[:hierarchy].path} xview", session.app.command("#{wrapper}.hsb", :cget, '-command')
163
+
164
+ session.app.destroy
165
+ end
166
+ end
167
+
168
+ def test_wrapped_widget_composes_correctly_inside_a_flow_container
169
+ assert_tk_app("the layout should pack/place the WRAPPER, not the raw widget, inside a flow container") do
170
+ require 'teek/ui'
171
+
172
+ session = Teek::UI.app(title: 'Native Scrollable Test') do |ui|
173
+ ui.column do |c|
174
+ c.label(text: 'Header')
175
+ c.list(:items, grow: true)
176
+ end
177
+ end
178
+ session.run_async
179
+ session.app.update
180
+
181
+ wrapper = session[:items].path.sub(/\.widget\z/, '')
182
+ assert_equal 'pack', session.app.tcl_eval("winfo manager #{wrapper}")
183
+ assert session.app.winfo.ismapped?(wrapper)
184
+
185
+ session.app.destroy
186
+ end
187
+ end
188
+
189
+ def test_app_level_scroll_false_overrides_the_global_default
190
+ assert_tk_app("Teek::UI.app(scroll: false) should suppress auto-attach app-wide") do
191
+ require 'teek/ui'
192
+
193
+ session = Teek::UI.app(title: 'Native Scrollable Test', scroll: false) { |ui| ui.list(:items) }
194
+ session.run_async
195
+ session.app.update
196
+
197
+ assert_equal '.items', session[:items].path
198
+ assert_equal '0', session.app.tcl_eval("winfo exists .items.vsb")
199
+
200
+ session.app.destroy
201
+ end
202
+ end
203
+
204
+ def test_per_widget_scroll_true_overrides_an_app_level_false
205
+ assert_tk_app("a widget's own scroll: true should win over the app-level default") do
206
+ require 'teek/ui'
207
+
208
+ session = Teek::UI.app(title: 'Native Scrollable Test', scroll: false) { |ui| ui.list(:items, scroll: true) }
209
+ session.run_async
210
+ session.app.update
211
+
212
+ wrapper = session[:items].path.sub(/\.widget\z/, '')
213
+ assert_equal '1', session.app.tcl_eval("winfo exists #{wrapper}.vsb")
214
+
215
+ session.app.destroy
216
+ end
217
+ end
218
+
219
+ def test_global_default_false_suppresses_auto_attach_unless_overridden
220
+ assert_tk_app("Teek::UI.auto_scroll = false should suppress the default everywhere it isn't overridden") do
221
+ require 'teek/ui'
222
+
223
+ # The test runner keeps one Tk worker process alive across every test
224
+ # in this file, reusing its already-loaded Teek::UI module - so a
225
+ # global class-level default mutated here would otherwise leak into
226
+ # every test that runs afterward in the same process. Always restore
227
+ # it, success or failure.
228
+ original = Teek::UI.auto_scroll
229
+ begin
230
+ Teek::UI.auto_scroll = false
231
+ session = Teek::UI.app(title: 'Native Scrollable Test') do |ui|
232
+ ui.list(:default_items)
233
+ ui.list(:forced_items, scroll: true)
234
+ end
235
+ session.run_async
236
+ session.app.update
237
+
238
+ assert_equal '.default_items', session[:default_items].path
239
+ assert_equal '0', session.app.tcl_eval("winfo exists .default_items.vsb")
240
+
241
+ forced_wrapper = session[:forced_items].path.sub(/\.widget\z/, '')
242
+ assert_equal '1', session.app.tcl_eval("winfo exists #{forced_wrapper}.vsb")
243
+
244
+ session.app.destroy
245
+ ensure
246
+ Teek::UI.auto_scroll = original
247
+ end
248
+ end
249
+ end
250
+ end
data/test/test_node.rb ADDED
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/node'
5
+ require 'teek/ui/scope'
6
+
7
+ class TestNode < Minitest::Test
8
+ def test_defaults
9
+ node = Teek::UI::Node.new(type: :button)
10
+
11
+ assert_equal :button, node.type
12
+ assert_nil node.name
13
+ assert_nil node.key
14
+ assert_equal({}, node.opts)
15
+ assert_equal [], node.children
16
+ assert_equal [], node.events
17
+ assert_nil node.layout
18
+ assert_nil node.realized
19
+ assert_same Teek::UI::Scope::TOP_LEVEL, node.scope
20
+ end
21
+
22
+ def test_scope_is_settable_and_readable
23
+ scope = Teek::UI::Scope.new(:sidebar)
24
+ node = Teek::UI::Node.new(type: :button, scope: scope)
25
+
26
+ assert_same scope, node.scope
27
+ end
28
+
29
+ def test_key_defaults_to_the_name_when_no_key_given
30
+ node = Teek::UI::Node.new(type: :button, name: :save)
31
+
32
+ assert_equal 'save', node.key
33
+ end
34
+
35
+ def test_explicit_key_overrides_the_name_derived_default
36
+ node = Teek::UI::Node.new(type: :button, name: :save, key: 'custom')
37
+
38
+ assert_equal 'custom', node.key
39
+ end
40
+
41
+ def test_opts_are_retained_verbatim
42
+ node = Teek::UI::Node.new(type: :label, opts: { text: 'Hi', width: 10 })
43
+
44
+ assert_equal({ text: 'Hi', width: 10 }, node.opts)
45
+ end
46
+
47
+ def test_add_child_appends_and_returns_the_child
48
+ parent = Teek::UI::Node.new(type: :column)
49
+ child = Teek::UI::Node.new(type: :button)
50
+
51
+ result = parent.add_child(child)
52
+
53
+ assert_equal [child], parent.children
54
+ assert_same child, result
55
+ end
56
+
57
+ def test_layout_and_realized_are_settable_after_construction
58
+ node = Teek::UI::Node.new(type: :button)
59
+
60
+ node.layout = { grow: true }
61
+ node.realized = :fake_handle
62
+
63
+ assert_equal({ grow: true }, node.layout)
64
+ assert_equal :fake_handle, node.realized
65
+ end
66
+
67
+ def test_each_visits_self_then_children_depth_first_preorder
68
+ root = Teek::UI::Node.new(type: :column)
69
+ a = root.add_child(Teek::UI::Node.new(type: :button, name: :a))
70
+ b = root.add_child(Teek::UI::Node.new(type: :column, name: :b))
71
+ c = b.add_child(Teek::UI::Node.new(type: :button, name: :c))
72
+
73
+ visited = []
74
+ root.each { |n| visited << n }
75
+
76
+ assert_equal [root, a, b, c], visited
77
+ end
78
+
79
+ def test_each_without_a_block_returns_an_enumerator
80
+ root = Teek::UI::Node.new(type: :column)
81
+ root.add_child(Teek::UI::Node.new(type: :button, name: :a))
82
+
83
+ assert_equal [:column, :button], root.each.map(&:type)
84
+ end
85
+
86
+ def test_add_child_sets_the_childs_parent
87
+ parent = Teek::UI::Node.new(type: :column)
88
+ child = Teek::UI::Node.new(type: :button)
89
+
90
+ parent.add_child(child)
91
+
92
+ assert_same parent, child.parent
93
+ end
94
+
95
+ def test_document_defaults_to_nil
96
+ node = Teek::UI::Node.new(type: :button)
97
+
98
+ assert_nil node.document
99
+ end
100
+
101
+ def test_document_is_settable_at_construction
102
+ fake_document = Object.new
103
+ node = Teek::UI::Node.new(type: :button, document: fake_document)
104
+
105
+ assert_same fake_document, node.document
106
+ end
107
+
108
+ def test_remove_child_removes_it_from_the_parents_children
109
+ parent = Teek::UI::Node.new(type: :column)
110
+ a = parent.add_child(Teek::UI::Node.new(type: :button, name: :a))
111
+ b = parent.add_child(Teek::UI::Node.new(type: :button, name: :b))
112
+
113
+ parent.remove_child(a)
114
+
115
+ assert_equal [b], parent.children
116
+ end
117
+
118
+ def test_remove_child_clears_the_removed_nodes_own_parent
119
+ parent = Teek::UI::Node.new(type: :column)
120
+ child = parent.add_child(Teek::UI::Node.new(type: :button))
121
+
122
+ parent.remove_child(child)
123
+
124
+ assert_nil child.parent
125
+ end
126
+
127
+ def test_remove_child_returns_the_removed_node
128
+ parent = Teek::UI::Node.new(type: :column)
129
+ child = parent.add_child(Teek::UI::Node.new(type: :button))
130
+
131
+ result = parent.remove_child(child)
132
+
133
+ assert_same child, result
134
+ end
135
+
136
+ def test_parent_is_nil_before_being_attached
137
+ node = Teek::UI::Node.new(type: :button)
138
+
139
+ assert_nil node.parent
140
+ end
141
+
142
+ def test_parent_setter_is_not_publicly_callable
143
+ node = Teek::UI::Node.new(type: :button)
144
+
145
+ assert_raises(NoMethodError) { node.parent = Teek::UI::Node.new(type: :column) }
146
+ end
147
+
148
+ def test_root_logical_path_is_a_bare_dot
149
+ root = Teek::UI::Node.new(type: :root)
150
+
151
+ assert_equal '.', root.logical_path
152
+ end
153
+
154
+ def test_logical_path_for_a_top_level_named_node
155
+ root = Teek::UI::Node.new(type: :root)
156
+ child = root.add_child(Teek::UI::Node.new(type: :button, name: :save))
157
+
158
+ assert_equal '.save', child.logical_path
159
+ end
160
+
161
+ def test_logical_path_nests_through_named_ancestors
162
+ root = Teek::UI::Node.new(type: :root)
163
+ column = root.add_child(Teek::UI::Node.new(type: :column, name: :toolbar))
164
+ button = column.add_child(Teek::UI::Node.new(type: :button, name: :save))
165
+
166
+ assert_equal '.toolbar.save', button.logical_path
167
+ end
168
+
169
+ def test_logical_path_uses_the_auto_generated_key_when_unnamed
170
+ root = Teek::UI::Node.new(type: :root)
171
+ child = root.add_child(Teek::UI::Node.new(type: :button, key: '#anon1'))
172
+
173
+ assert_equal '.#anon1', child.logical_path
174
+ end
175
+
176
+ def test_logical_path_of_an_unattached_node_treats_it_as_top_level
177
+ node = Teek::UI::Node.new(type: :button, name: :save)
178
+
179
+ assert_equal '.save', node.logical_path
180
+ end
181
+
182
+ def test_display_name_is_the_bare_type_when_unnamed
183
+ node = Teek::UI::Node.new(type: :column)
184
+
185
+ assert_equal 'column', node.display_name
186
+ end
187
+
188
+ def test_display_name_includes_the_name_when_given
189
+ node = Teek::UI::Node.new(type: :column, name: :ctrl)
190
+
191
+ assert_equal 'column(:ctrl)', node.display_name
192
+ end
193
+ end