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,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
|