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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/README.md +452 -0
- data/lib/teek/ui/canvas_item.rb +270 -0
- data/lib/teek/ui/component_handle.rb +32 -0
- data/lib/teek/ui/document.rb +185 -0
- data/lib/teek/ui/errors.rb +41 -0
- data/lib/teek/ui/event_binding.rb +22 -0
- data/lib/teek/ui/event_bus.rb +53 -0
- data/lib/teek/ui/grid_validator.rb +89 -0
- data/lib/teek/ui/handle.rb +585 -0
- data/lib/teek/ui/image.rb +58 -0
- data/lib/teek/ui/keysyms.rb +72 -0
- data/lib/teek/ui/menu_builder.rb +130 -0
- data/lib/teek/ui/menu_entry_addressing.rb +62 -0
- data/lib/teek/ui/modal_stack.rb +99 -0
- data/lib/teek/ui/mouse_events.rb +31 -0
- data/lib/teek/ui/node.rb +141 -0
- data/lib/teek/ui/option_dump_parsing.rb +31 -0
- data/lib/teek/ui/overlay_anchors.rb +29 -0
- data/lib/teek/ui/overlay_validator.rb +33 -0
- data/lib/teek/ui/pane_validator.rb +30 -0
- data/lib/teek/ui/realized_node.rb +25 -0
- data/lib/teek/ui/realizer.rb +577 -0
- data/lib/teek/ui/scope.rb +40 -0
- data/lib/teek/ui/screens.rb +141 -0
- data/lib/teek/ui/session.rb +417 -0
- data/lib/teek/ui/tab_validator.rb +30 -0
- data/lib/teek/ui/text_content.rb +321 -0
- data/lib/teek/ui/tree_inspector.rb +97 -0
- data/lib/teek/ui/validator.rb +131 -0
- data/lib/teek/ui/var.rb +98 -0
- data/lib/teek/ui/version.rb +7 -0
- data/lib/teek/ui/widget_addressing.rb +47 -0
- data/lib/teek/ui/widget_dsl.rb +512 -0
- data/lib/teek/ui/widget_type.rb +242 -0
- data/lib/teek/ui/widget_types/button.rb +7 -0
- data/lib/teek/ui/widget_types/canvas.rb +15 -0
- data/lib/teek/ui/widget_types/checkbox.rb +7 -0
- data/lib/teek/ui/widget_types/column.rb +14 -0
- data/lib/teek/ui/widget_types/context_menu.rb +15 -0
- data/lib/teek/ui/widget_types/divider.rb +10 -0
- data/lib/teek/ui/widget_types/dropdown.rb +7 -0
- data/lib/teek/ui/widget_types/grid.rb +12 -0
- data/lib/teek/ui/widget_types/group.rb +7 -0
- data/lib/teek/ui/widget_types/label.rb +7 -0
- data/lib/teek/ui/widget_types/list.rb +7 -0
- data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
- data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
- data/lib/teek/ui/widget_types/menu_item.rb +20 -0
- data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
- data/lib/teek/ui/widget_types/number_box.rb +7 -0
- data/lib/teek/ui/widget_types/pane.rb +37 -0
- data/lib/teek/ui/widget_types/panel.rb +7 -0
- data/lib/teek/ui/widget_types/progress.rb +7 -0
- data/lib/teek/ui/widget_types/radio.rb +7 -0
- data/lib/teek/ui/widget_types/row.rb +14 -0
- data/lib/teek/ui/widget_types/scrollable.rb +17 -0
- data/lib/teek/ui/widget_types/slider.rb +7 -0
- data/lib/teek/ui/widget_types/spacer.rb +14 -0
- data/lib/teek/ui/widget_types/split.rb +17 -0
- data/lib/teek/ui/widget_types/tab.rb +35 -0
- data/lib/teek/ui/widget_types/table.rb +9 -0
- data/lib/teek/ui/widget_types/tabs.rb +10 -0
- data/lib/teek/ui/widget_types/text_area.rb +7 -0
- data/lib/teek/ui/widget_types/text_box.rb +7 -0
- data/lib/teek/ui/widget_types/tree.rb +9 -0
- data/lib/teek/ui/widget_types/window.rb +50 -0
- data/lib/teek/ui/widget_types.rb +121 -0
- data/lib/teek/ui/widget_validators.rb +62 -0
- data/lib/teek/ui.rb +54 -0
- data/teek-ui.gemspec +24 -0
- data/test/support/fake_app.rb +57 -0
- data/test/test_busy.rb +82 -0
- data/test/test_canvas_items.rb +551 -0
- data/test/test_clipboard.rb +87 -0
- data/test/test_close_handling.rb +118 -0
- data/test/test_component.rb +151 -0
- data/test/test_component_realtk.rb +120 -0
- data/test/test_debug_info.rb +158 -0
- data/test/test_document.rb +317 -0
- data/test/test_event_bus.rb +163 -0
- data/test/test_events.rb +164 -0
- data/test/test_fake_app_contract.rb +62 -0
- data/test/test_grid.rb +97 -0
- data/test/test_handle.rb +386 -0
- data/test/test_handle_destroy_realtk.rb +344 -0
- data/test/test_helper.rb +20 -0
- data/test/test_image.rb +24 -0
- data/test/test_image_realtk.rb +146 -0
- data/test/test_incremental_realize.rb +203 -0
- data/test/test_keysyms.rb +76 -0
- data/test/test_layout.rb +185 -0
- data/test/test_lazy_realize.rb +128 -0
- data/test/test_managed_window.rb +304 -0
- data/test/test_menu_dsl.rb +230 -0
- data/test/test_menu_entry_addressing.rb +73 -0
- data/test/test_menu_realize.rb +296 -0
- data/test/test_modal_handle.rb +147 -0
- data/test/test_modal_stack.rb +219 -0
- data/test/test_modal_stack_realtk.rb +131 -0
- data/test/test_native_scrollable.rb +250 -0
- data/test/test_node.rb +193 -0
- data/test/test_overlay.rb +139 -0
- data/test/test_raw.rb +58 -0
- data/test/test_reactive_vars.rb +118 -0
- data/test/test_realizer.rb +264 -0
- data/test/test_scope.rb +38 -0
- data/test/test_screens.rb +260 -0
- data/test/test_screens_realtk.rb +314 -0
- data/test/test_scrollable.rb +185 -0
- data/test/test_scrollable_wheel.rb +172 -0
- data/test/test_scrollbar_auto_hide.rb +127 -0
- data/test/test_split.rb +145 -0
- data/test/test_tabs.rb +201 -0
- data/test/test_text_content.rb +664 -0
- data/test/test_toast.rb +126 -0
- data/test/test_tree_inspector.rb +183 -0
- data/test/test_ui.rb +428 -0
- data/test/test_validator.rb +209 -0
- data/test/test_var.rb +32 -0
- data/test/test_widget_dsl.rb +621 -0
- data/test/test_widget_introspection.rb +126 -0
- data/test/test_widget_types.rb +429 -0
- data/test/test_widget_types_realtk.rb +82 -0
- data/test/test_widget_validators.rb +82 -0
- 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
|