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,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Under Xvfb there's no real window manager reliably sending a genuine
7
+ # WM_DELETE_WINDOW client message - these read back the script on_close
8
+ # registered via `wm protocol` and eval it directly, the same "read back
9
+ # and invoke" approach already used elsewhere in this suite (canvas item
10
+ # bindings, treeview heading commands) and in the base teek gem's own
11
+ # on_close tests.
12
+
13
+ class TestCloseHandling < Minitest::Test
14
+ include TeekTestHelper
15
+
16
+ def test_on_close_fires_on_a_simulated_close
17
+ assert_tk_app("win.on_close should fire when WM_DELETE_WINDOW is simulated") do
18
+ require 'teek/ui'
19
+
20
+ closed = false
21
+ session = Teek::UI.app(title: 'Close Handling Test') do |ui|
22
+ ui.window(:settings).on_close { closed = true }
23
+ end
24
+ session.run_async
25
+ session.app.update
26
+
27
+ path = session[:settings].path
28
+ script = session.app.tcl_eval("wm protocol #{path} WM_DELETE_WINDOW")
29
+ session.app.tcl_eval(script)
30
+
31
+ assert closed, "on_close block did not fire"
32
+
33
+ session.app.destroy
34
+ end
35
+ end
36
+
37
+ def test_on_close_does_not_auto_destroy_the_window
38
+ assert_tk_app("on_close should not implicitly destroy the window - that's the block's own call") do
39
+ require 'teek/ui'
40
+
41
+ session = Teek::UI.app(title: 'Close Handling Test') do |ui|
42
+ ui.window(:settings).on_close { }
43
+ end
44
+ session.run_async
45
+ session.app.update
46
+
47
+ path = session[:settings].path
48
+ script = session.app.tcl_eval("wm protocol #{path} WM_DELETE_WINDOW")
49
+ session.app.tcl_eval(script)
50
+
51
+ assert session.app.winfo.exists?(path), "on_close must not destroy the window itself unless the block chooses to"
52
+
53
+ session.app.destroy
54
+ end
55
+ end
56
+
57
+ def test_on_close_block_can_choose_to_destroy_the_window
58
+ assert_tk_app("an on_close block that destroys the window should actually close it") do
59
+ require 'teek/ui'
60
+
61
+ session = Teek::UI.app(title: 'Close Handling Test') do |ui|
62
+ ui.window(:settings).on_close { session.app.destroy(session[:settings].path) }
63
+ end
64
+ session.run_async
65
+ session.app.update
66
+
67
+ path = session[:settings].path
68
+ script = session.app.tcl_eval("wm protocol #{path} WM_DELETE_WINDOW")
69
+ session.app.tcl_eval(script)
70
+
71
+ refute session.app.winfo.exists?(path)
72
+
73
+ session.app.destroy
74
+ end
75
+ end
76
+
77
+ def test_rebinding_on_close_replaces_rather_than_leaks
78
+ assert_tk_app("calling on_close again should replace the handler, not accumulate a callback") do
79
+ require 'teek/ui'
80
+
81
+ session = Teek::UI.app(title: 'Close Handling Test') do |ui|
82
+ ui.window(:settings).on_close { }
83
+ end
84
+ session.run_async
85
+ session.app.update
86
+ baseline = session.app.interp.callback_ids.length
87
+
88
+ fired_new = false
89
+ session[:settings].on_close { fired_new = true }
90
+
91
+ assert_equal baseline, session.app.interp.callback_ids.length,
92
+ "rebinding on_close should replace, not accumulate, the tracked callback"
93
+
94
+ path = session[:settings].path
95
+ script = session.app.tcl_eval("wm protocol #{path} WM_DELETE_WINDOW")
96
+ session.app.tcl_eval(script)
97
+
98
+ assert fired_new, "the replaced on_close handler should be the one that fires"
99
+
100
+ session.app.destroy
101
+ end
102
+ end
103
+
104
+ def test_on_close_raises_on_a_non_window_handle
105
+ assert_tk_app("on_close on a non-window handle should raise a clear error") do
106
+ require 'teek/ui'
107
+
108
+ session = Teek::UI.app(title: 'Close Handling Test') { |ui| ui.button(:go, text: 'Go') }
109
+ session.run_async
110
+ session.app.update
111
+
112
+ error = assert_raises(ArgumentError) { session[:go].on_close { } }
113
+ assert_match(/window/i, error.message)
114
+
115
+ session.app.destroy
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/session'
5
+
6
+ # ui.component opens a fresh Scope around its block, so the SAME local
7
+ # name declared in two different components never collides - a plain
8
+ # threaded-builder method (def foo(ui) = ui.row { ... }) needs none of
9
+ # this and keeps working unchanged; ui.component is only for the
10
+ # opt-in case where scope isolation matters (reuse, avoiding name
11
+ # collisions across files).
12
+ class TestComponent < Minitest::Test
13
+ def build_session
14
+ Teek::UI::Session.new(title: 'Component Test')
15
+ end
16
+
17
+ def test_component_splices_its_subtree_under_the_current_parent
18
+ session = build_session
19
+
20
+ session.panel(:sidebar) { |p| p.component { |c| c.button(:save, text: 'Save') } }
21
+
22
+ sidebar_node = session.document.root.children.first
23
+ assert_equal :panel, sidebar_node.type
24
+ assert_equal [:button], sidebar_node.children.map(&:type),
25
+ "the component's own node should attach directly to :sidebar, with no extra wrapper container"
26
+ end
27
+
28
+ def test_the_same_local_name_in_two_different_components_does_not_collide
29
+ session = build_session
30
+
31
+ session.component { |c| c.button(:save, text: 'Save A') }
32
+ session.component { |c| c.button(:save, text: 'Save B') }
33
+
34
+ # neither #component call raised - that's the assertion. Confirm both
35
+ # nodes actually exist, independently, under the document root.
36
+ assert_equal 2, session.document.root.children.length
37
+ assert_equal ['Save A', 'Save B'], session.document.root.children.map { |n| n.opts[:text] }
38
+ end
39
+
40
+ def test_ui_bracket_inside_a_component_resolves_to_that_components_own_node
41
+ session = build_session
42
+ inner_lookup = nil
43
+
44
+ session.component do |c|
45
+ c.button(:save, text: 'Save')
46
+ inner_lookup = c[:save]
47
+ end
48
+
49
+ refute_nil inner_lookup
50
+ assert_equal :button, inner_lookup.type
51
+ end
52
+
53
+ def test_ui_bracket_inside_a_component_does_not_see_a_top_level_node_of_the_same_name
54
+ session = build_session
55
+ session.button(:save, text: 'Top-level Save')
56
+ inner_lookup = :not_set
57
+
58
+ session.component { |c| inner_lookup = c[:save] }
59
+
60
+ assert_nil inner_lookup, "a component's ui[:save] should not resolve to an unrelated top-level :save"
61
+ end
62
+
63
+ def test_ui_bracket_outside_any_component_does_not_see_a_components_node
64
+ session = build_session
65
+
66
+ session.component { |c| c.button(:save, text: 'Component Save') }
67
+
68
+ assert_nil session[:save], "top-level ui[:save] should not resolve into a component's own scope"
69
+ end
70
+
71
+ def test_ui_bracket_at_top_level_is_unaffected_by_components_existing
72
+ session = build_session
73
+ session.button(:go, text: 'Go')
74
+ session.component { |c| c.button(:save, text: 'Save') }
75
+
76
+ handle = session[:go]
77
+
78
+ refute_nil handle
79
+ assert_equal :button, handle.type
80
+ end
81
+
82
+ def test_two_components_can_use_the_same_label_without_colliding
83
+ session = build_session
84
+
85
+ session.component(:row) { |c| c.button(:save, text: 'First') }
86
+ session.component(:row) { |c| c.button(:save, text: 'Second') }
87
+
88
+ assert_equal 2, session.document.root.children.length
89
+ assert_equal ['First', 'Second'], session.document.root.children.map { |n| n.opts[:text] }
90
+ end
91
+
92
+ def test_threaded_builder_style_still_works_with_no_component_call_at_all
93
+ session = build_session
94
+ toolbar = ->(ui) { ui.row { |r| r.button(:save, text: 'Save') } }
95
+
96
+ session.panel(:p) { |p| toolbar.call(p) }
97
+
98
+ assert_equal :button, session[:save].type
99
+ end
100
+
101
+ def test_component_requires_no_tk_interpreter_to_exercise
102
+ session = build_session
103
+
104
+ session.component { |c| c.button(:save, text: 'Save') }
105
+
106
+ # reaching this line with no Teek::App/interpreter involved anywhere
107
+ # above IS the assertion - scoping is pure Ruby.
108
+ assert session.document.root.children.any?
109
+ end
110
+
111
+ def test_component_returns_a_facade_that_resolves_its_own_named_handle
112
+ session = build_session
113
+
114
+ facade = session.component { |c| c.button(:save, text: 'Save') }
115
+
116
+ handle = facade.handle(:save)
117
+ refute_nil handle
118
+ assert_equal :button, handle.type
119
+ end
120
+
121
+ def test_facade_bracket_is_an_alias_for_handle
122
+ session = build_session
123
+
124
+ facade = session.component { |c| c.button(:save, text: 'Save') }
125
+
126
+ assert_equal facade.handle(:save).type, facade[:save].type
127
+ refute_nil facade[:save]
128
+ end
129
+
130
+ def test_facade_returns_nil_for_a_name_the_component_never_declared
131
+ session = build_session
132
+
133
+ facade = session.component { |c| c.button(:save, text: 'Save') }
134
+
135
+ assert_nil facade[:not_declared]
136
+ end
137
+
138
+ def test_facade_still_resolves_its_own_names_when_mounted_under_different_parents
139
+ session = build_session
140
+
141
+ facade_a = nil
142
+ facade_b = nil
143
+ session.panel(:left) { |p| facade_a = p.component { |c| c.button(:save, text: 'Left Save') } }
144
+ session.panel(:right) { |p| facade_b = p.component { |c| c.button(:save, text: 'Right Save') } }
145
+
146
+ refute_nil facade_a[:save]
147
+ refute_nil facade_b[:save]
148
+ assert_equal :button, facade_a[:save].type
149
+ assert_equal :button, facade_b[:save].type
150
+ end
151
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Real-Tk half of ui.component's coverage - scoping itself (name
7
+ # collisions, ui[:name] resolution) is pure Ruby, covered headlessly in
8
+ # test_component.rb; this confirms the realized side: two components'
9
+ # like-named children land at distinct, real Tk paths with no extra work
10
+ # needed from Realizer - allocate_path already builds a path from each
11
+ # node's OWN parent chain, and two components mounted under different
12
+ # parents are never Tk siblings of each other.
13
+ class TestComponentRealTk < Minitest::Test
14
+ include TeekTestHelper
15
+
16
+ def test_two_components_like_named_children_get_distinct_tk_paths
17
+ assert_tk_app("two components under different parents should realize distinct paths for their like-named children") do
18
+ require 'teek/ui'
19
+
20
+ save_a = nil
21
+ save_b = nil
22
+ session = Teek::UI.app(title: 'Component Test') do |ui|
23
+ ui.panel(:sidebar) { |p| p.component { |c| save_a = c.button(:save, text: 'Save A') } }
24
+ ui.panel(:main) { |p| p.component { |c| save_b = c.button(:save, text: 'Save B') } }
25
+ end
26
+ session.run_async
27
+ session.app.update
28
+
29
+ assert_equal "#{session[:sidebar].path}.save", save_a.path
30
+ assert_equal "#{session[:main].path}.save", save_b.path
31
+ refute_equal save_a.path, save_b.path
32
+
33
+ assert session.app.winfo.exists?(save_a.path)
34
+ assert session.app.winfo.exists?(save_b.path)
35
+ assert_equal 'Save A', session.app.command(save_a.path, :cget, '-text')
36
+ assert_equal 'Save B', session.app.command(save_b.path, :cget, '-text')
37
+
38
+ session.app.destroy
39
+ end
40
+ end
41
+
42
+ def test_a_components_facade_only_ever_resolves_that_components_own_node
43
+ assert_tk_app("a component's facade should resolve to its own :save, never a sibling component's like-named one") do
44
+ require 'teek/ui'
45
+
46
+ facade_a = nil
47
+ facade_b = nil
48
+ session = Teek::UI.app(title: 'Component Test') do |ui|
49
+ ui.panel(:sidebar) { |p| facade_a = p.component { |c| c.button(:save, text: 'Save A') } }
50
+ ui.panel(:main) { |p| facade_b = p.component { |c| c.button(:save, text: 'Save B') } }
51
+ end
52
+ session.run_async
53
+ session.app.update
54
+
55
+ handle_a = facade_a[:save]
56
+ handle_b = facade_b[:save]
57
+
58
+ refute_equal handle_a.path, handle_b.path
59
+ assert_equal 'Save A', session.app.command(handle_a.path, :cget, '-text')
60
+ assert_equal 'Save B', session.app.command(handle_b.path, :cget, '-text')
61
+
62
+ handle_a.configure(text: 'Changed A')
63
+ assert_equal 'Changed A', session.app.command(handle_a.path, :cget, '-text')
64
+ assert_equal 'Save B', session.app.command(handle_b.path, :cget, '-text'),
65
+ "mutating component A's facade handle should never affect component B's widget"
66
+
67
+ session.app.destroy
68
+ end
69
+ end
70
+
71
+ def test_mounting_the_same_component_three_times_under_one_parent_gets_three_distinct_paths
72
+ assert_tk_app("mounting the same component 3x directly under one parent (not each under its own sub-panel) should realize 3 distinct :save widgets, each addressable through its own mount's facade") do
73
+ require 'teek/ui'
74
+
75
+ row = ->(ui, label) { ui.component { |c| c.button(:save, text: label) } }
76
+
77
+ facades = []
78
+ session = Teek::UI.app(title: 'Component Test') do |ui|
79
+ ui.panel(:list) { |p| 3.times { |i| facades << row.call(p, "Row #{i + 1}") } }
80
+ end
81
+ session.run_async
82
+ session.app.update
83
+
84
+ paths = facades.map { |f| f[:save].path }
85
+ assert_equal 3, paths.uniq.length, "each mount's :save should realize its own distinct Tk path (got #{paths.inspect})"
86
+ assert_equal ['Row 1', 'Row 2', 'Row 3'], paths.map { |path| session.app.command(path, :cget, '-text') }
87
+
88
+ facades[1][:save].configure(text: 'Changed')
89
+ assert_equal 'Row 1', session.app.command(paths[0], :cget, '-text')
90
+ assert_equal 'Changed', session.app.command(paths[1], :cget, '-text'),
91
+ "configuring through the second mount's facade should mutate the second mount's own widget"
92
+ assert_equal 'Row 3', session.app.command(paths[2], :cget, '-text')
93
+
94
+ session.app.destroy
95
+ end
96
+ end
97
+
98
+ def test_an_event_bound_inside_a_repeated_mount_fires_for_that_mounts_own_widget_only
99
+ assert_tk_app("a click handler declared inside one mount of a 3x-repeated component should fire only for that mount's own widget, never a sibling mount's") do
100
+ require 'teek/ui'
101
+
102
+ fired = []
103
+ row = ->(ui, label) { ui.component { |c| c.button(:save, text: label).on_click { fired << label } } }
104
+
105
+ facades = []
106
+ session = Teek::UI.app(title: 'Component Test') do |ui|
107
+ ui.panel(:list) { |p| 3.times { |i| facades << row.call(p, "Row #{i + 1}") } }
108
+ end
109
+ session.run_async
110
+ session.app.update
111
+
112
+ session.app.tcl_eval("event generate #{facades[1][:save].path} <Button-1>")
113
+ session.app.update
114
+
115
+ assert_equal ['Row 2'], fired, "clicking the second mount's widget should fire only its own handler"
116
+
117
+ session.app.destroy
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Aggregate callback/leak diagnostics: Session#debug_info (programmatic)
7
+ # and run/run_async's debug: true (prints the same summary to STDERR).
8
+ # Both are thin wrappers over Teek::CallbackRegistry#counts_by_tag,
9
+ # which already has its own dedicated, headless coverage in teek core's
10
+ # own test/test_callback_registry.rb - these focus on teek-ui's own
11
+ # wiring (realize-only guard, friendly key names, the debug: printout),
12
+ # not re-proving the registry's own counting logic.
13
+ class TestDebugInfo < Minitest::Test
14
+ include TeekTestHelper
15
+
16
+ def test_debug_info_raises_before_realize
17
+ assert_tk_app("session.debug_info should raise before realize, matching every other realize-only diagnostic") do
18
+ require 'teek/ui'
19
+
20
+ session = Teek::UI.app(title: 'Debug Info Test')
21
+
22
+ assert_raises(Teek::UI::NotRealizedError) { session.debug_info }
23
+ end
24
+ end
25
+
26
+ def test_debug_info_reports_an_event_binding
27
+ assert_tk_app("debug_info should count a wired on_click as an event binding") do
28
+ require 'teek/ui'
29
+
30
+ session = Teek::UI.app(title: 'Debug Info Test') { |ui| ui.button(:go, text: 'Go').on_click { } }
31
+ session.run_async
32
+ session.app.update
33
+
34
+ assert_equal 1, session.debug_info[:event_bindings]
35
+
36
+ session.app.destroy
37
+ end
38
+ end
39
+
40
+ def test_debug_info_reports_a_menu_entry
41
+ assert_tk_app("debug_info should count a menu item's own command callback as a menu entry") do
42
+ require 'teek/ui'
43
+
44
+ session = Teek::UI.app(title: 'Debug Info Test') do |ui|
45
+ ui.menu_bar { |mb| mb.menu(:file, label: 'File') { |f| f.item(label: 'Open') { } } }
46
+ end
47
+ session.run_async
48
+ session.app.update
49
+
50
+ assert_equal 1, session.debug_info[:menu_entries]
51
+
52
+ session.app.destroy
53
+ end
54
+ end
55
+
56
+ def test_debug_info_reports_a_canvas_item_bind
57
+ assert_tk_app("debug_info should count a canvas item's on_click as a canvas item bind") do
58
+ require 'teek/ui'
59
+
60
+ session = Teek::UI.app(title: 'Debug Info Test') { |ui| ui.canvas(:board, width: 100, height: 100) }
61
+ session.run_async
62
+ session.app.update
63
+ session[:board].oval(0, 0, 10, 10).on_click { }
64
+
65
+ assert_equal 1, session.debug_info[:canvas_item_binds]
66
+
67
+ session.app.destroy
68
+ end
69
+ end
70
+
71
+ def test_debug_info_reports_a_widget_option_callback
72
+ assert_tk_app("debug_info should count a bare -command option (e.g. from a Var bind:) as a widget-option callback") do
73
+ require 'teek/ui'
74
+
75
+ session = Teek::UI.app(title: 'Debug Info Test') do |ui|
76
+ wrap = ui.var(false)
77
+ ui.checkbox(:agree, bind: wrap)
78
+ end
79
+ session.run_async
80
+ session.app.update
81
+ # a plain -variable bind doesn't register a Ruby callback on its
82
+ # own - drive a real -command option through the escape hatch to
83
+ # exercise this category directly, matching what App#command's own
84
+ # -command handling (the :widget_option tag) actually tracks.
85
+ session.app.command(session[:agree].path, :configure, command: -> { })
86
+
87
+ assert_equal 1, session.debug_info[:widget_option_callbacks]
88
+
89
+ session.app.destroy
90
+ end
91
+ end
92
+
93
+ def test_debug_info_returns_to_baseline_after_destroying_the_widgets
94
+ assert_tk_app("creating N event bindings then destroying those widgets should return the count to baseline, not just count monotonically upward") do
95
+ require 'teek/ui'
96
+
97
+ session = Teek::UI.app(title: 'Debug Info Test') { |ui| ui.panel(:host) }
98
+ session.run_async
99
+ session.app.update
100
+ baseline = session.debug_info[:event_bindings] || 0
101
+
102
+ handles = []
103
+ session.add(:host) do |a|
104
+ 3.times { |i| handles << a.button(text: "Item #{i}").on_click { } }
105
+ end
106
+ session.app.update
107
+
108
+ assert_equal baseline + 3, session.debug_info[:event_bindings]
109
+
110
+ handles.each { |h| h.destroy!(defer: false) }
111
+ session.app.update
112
+
113
+ assert_equal baseline, session.debug_info[:event_bindings] || 0,
114
+ "destroying the widgets should release their callbacks, bringing the count back to baseline"
115
+
116
+ session.app.destroy
117
+ end
118
+ end
119
+
120
+ def test_run_async_debug_true_prints_a_summary_to_stderr
121
+ assert_tk_app("run_async(debug: true) should print the same grouped summary to stderr right after realize") do
122
+ require 'teek/ui'
123
+
124
+ _out, err = capture_io do
125
+ session = Teek::UI.app(title: 'Debug Info Test') { |ui| ui.button(:go, text: 'Go').on_click { } }
126
+ session.run_async(debug: true)
127
+ session.app.destroy
128
+ end
129
+
130
+ assert_match(/event_bindings/, err)
131
+ assert_match(/1/, err)
132
+ end
133
+ end
134
+
135
+ def test_run_async_without_debug_prints_nothing
136
+ assert_tk_app("run_async with no debug: (default false) should print nothing") do
137
+ require 'teek/ui'
138
+
139
+ _out, err = capture_io do
140
+ session = Teek::UI.app(title: 'Debug Info Test') { |ui| ui.button(:go, text: 'Go').on_click { } }
141
+ session.run_async
142
+ session.app.destroy
143
+ end
144
+
145
+ assert_empty err
146
+ end
147
+ end
148
+
149
+ # #run's own "print before and after mainloop" isn't covered by a
150
+ # live, blocking-mainloop test here - there's no existing precedent in
151
+ # this suite for scheduling a self-destroy to unblock #run from inside
152
+ # a test, and #run/#run_async share the exact same
153
+ # realize -> show -> print_debug_info sequence up to the point #run
154
+ # additionally calls #mainloop, which run_async's own debug: tests
155
+ # above already cover. The second print (after mainloop returns) is
156
+ # the same print_debug_info call, unconditionally placed right after
157
+ # the #mainloop line - see Session#run's own source.
158
+ end