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,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek/ui/document'
|
|
5
|
+
require 'teek/ui/scope'
|
|
6
|
+
require 'teek/ui/realized_node'
|
|
7
|
+
|
|
8
|
+
class TestDocument < Minitest::Test
|
|
9
|
+
def test_root_is_an_empty_root_node
|
|
10
|
+
document = Teek::UI::Document.new
|
|
11
|
+
|
|
12
|
+
assert_kind_of Teek::UI::Node, document.root
|
|
13
|
+
assert_equal :root, document.root.type
|
|
14
|
+
assert_equal [], document.root.children
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_create_builds_a_node_but_does_not_attach_it_to_any_parent
|
|
18
|
+
document = Teek::UI::Document.new
|
|
19
|
+
|
|
20
|
+
node = document.create(type: :button, opts: { text: 'Go' })
|
|
21
|
+
|
|
22
|
+
assert_equal :button, node.type
|
|
23
|
+
assert_equal({ text: 'Go' }, node.opts)
|
|
24
|
+
assert_equal [], document.root.children, "create should not attach the node anywhere - the caller decides the parent"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_named_node_is_findable_by_symbol_after_attaching
|
|
28
|
+
document = Teek::UI::Document.new
|
|
29
|
+
node = document.create(type: :button, name: :save)
|
|
30
|
+
document.root.add_child(node)
|
|
31
|
+
|
|
32
|
+
assert_same node, document.find(:save)
|
|
33
|
+
assert_same node, document[:save]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_create_gives_the_node_a_back_reference_to_the_document
|
|
37
|
+
document = Teek::UI::Document.new
|
|
38
|
+
|
|
39
|
+
node = document.create(type: :button, name: :save)
|
|
40
|
+
|
|
41
|
+
assert_same document, node.document
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_unregister_removes_a_named_node_from_the_index
|
|
45
|
+
document = Teek::UI::Document.new
|
|
46
|
+
node = document.create(type: :button, name: :save)
|
|
47
|
+
document.root.add_child(node)
|
|
48
|
+
|
|
49
|
+
document.unregister(node)
|
|
50
|
+
|
|
51
|
+
assert_nil document.find(:save)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_unregister_frees_the_name_for_reuse
|
|
55
|
+
document = Teek::UI::Document.new
|
|
56
|
+
first = document.create(type: :button, name: :save)
|
|
57
|
+
document.root.add_child(first)
|
|
58
|
+
document.unregister(first)
|
|
59
|
+
|
|
60
|
+
second = document.create(type: :button, name: :save)
|
|
61
|
+
|
|
62
|
+
assert_same second, document.find(:save)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_unregister_on_an_unnamed_node_is_a_safe_no_op
|
|
66
|
+
document = Teek::UI::Document.new
|
|
67
|
+
node = document.create(type: :button)
|
|
68
|
+
|
|
69
|
+
document.unregister(node)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_unregister_only_affects_the_given_nodes_own_scope
|
|
73
|
+
document = Teek::UI::Document.new
|
|
74
|
+
scope = Teek::UI::Scope.new(:a)
|
|
75
|
+
top_level = document.create(type: :button, name: :save)
|
|
76
|
+
document.root.add_child(top_level)
|
|
77
|
+
scoped = document.create(type: :button, name: :save, scope: scope)
|
|
78
|
+
|
|
79
|
+
document.unregister(scoped)
|
|
80
|
+
|
|
81
|
+
assert_same top_level, document.find(:save)
|
|
82
|
+
assert_nil document.find(:save, scope: scope)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_find_returns_nil_for_an_unknown_name
|
|
86
|
+
document = Teek::UI::Document.new
|
|
87
|
+
|
|
88
|
+
assert_nil document.find(:nope)
|
|
89
|
+
assert_nil document[:nope]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_unnamed_nodes_get_a_distinct_auto_generated_key
|
|
93
|
+
document = Teek::UI::Document.new
|
|
94
|
+
|
|
95
|
+
a = document.create(type: :button)
|
|
96
|
+
b = document.create(type: :button)
|
|
97
|
+
|
|
98
|
+
refute_nil a.key
|
|
99
|
+
refute_nil b.key
|
|
100
|
+
refute_equal a.key, b.key
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_named_node_key_is_the_name
|
|
104
|
+
document = Teek::UI::Document.new
|
|
105
|
+
|
|
106
|
+
node = document.create(type: :button, name: :save)
|
|
107
|
+
|
|
108
|
+
assert_equal 'save', node.key
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_duplicate_explicit_name_is_detected
|
|
112
|
+
document = Teek::UI::Document.new
|
|
113
|
+
document.create(type: :button, name: :save)
|
|
114
|
+
|
|
115
|
+
error = assert_raises(ArgumentError) { document.create(type: :button, name: :save) }
|
|
116
|
+
assert_match(/save/, error.message)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_same_name_in_two_different_scopes_does_not_collide
|
|
120
|
+
document = Teek::UI::Document.new
|
|
121
|
+
scope_a = Teek::UI::Scope.new(:a)
|
|
122
|
+
scope_b = Teek::UI::Scope.new(:b)
|
|
123
|
+
|
|
124
|
+
a = document.create(type: :button, name: :save, scope: scope_a)
|
|
125
|
+
b = document.create(type: :button, name: :save, scope: scope_b)
|
|
126
|
+
|
|
127
|
+
refute_same a, b
|
|
128
|
+
assert_same a, document.find(:save, scope: scope_a)
|
|
129
|
+
assert_same b, document.find(:save, scope: scope_b)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_same_name_in_the_same_scope_still_collides
|
|
133
|
+
document = Teek::UI::Document.new
|
|
134
|
+
scope = Teek::UI::Scope.new(:a)
|
|
135
|
+
document.create(type: :button, name: :save, scope: scope)
|
|
136
|
+
|
|
137
|
+
error = assert_raises(ArgumentError) {
|
|
138
|
+
document.create(type: :button, name: :save, scope: scope)
|
|
139
|
+
}
|
|
140
|
+
assert_match(/save/, error.message)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_scoped_name_does_not_collide_with_the_same_name_at_top_level
|
|
144
|
+
document = Teek::UI::Document.new
|
|
145
|
+
scope = Teek::UI::Scope.new(:a)
|
|
146
|
+
|
|
147
|
+
top_level = document.create(type: :button, name: :save)
|
|
148
|
+
scoped = document.create(type: :button, name: :save, scope: scope)
|
|
149
|
+
|
|
150
|
+
refute_same top_level, scoped
|
|
151
|
+
assert_same top_level, document.find(:save)
|
|
152
|
+
assert_same scoped, document.find(:save, scope: scope)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_find_with_a_scope_does_not_find_a_top_level_node_of_the_same_name
|
|
156
|
+
document = Teek::UI::Document.new
|
|
157
|
+
document.create(type: :button, name: :save)
|
|
158
|
+
|
|
159
|
+
assert_nil document.find(:save, scope: Teek::UI::Scope.new(:a))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_find_without_a_scope_does_not_find_a_scoped_node_of_the_same_name
|
|
163
|
+
document = Teek::UI::Document.new
|
|
164
|
+
document.create(type: :button, name: :save, scope: Teek::UI::Scope.new(:a))
|
|
165
|
+
|
|
166
|
+
assert_nil document.find(:save)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def test_two_scopes_with_the_same_label_do_not_share_a_namespace
|
|
170
|
+
document = Teek::UI::Document.new
|
|
171
|
+
first = Teek::UI::Scope.new(:widget)
|
|
172
|
+
second = Teek::UI::Scope.new(:widget)
|
|
173
|
+
|
|
174
|
+
a = document.create(type: :button, name: :save, scope: first)
|
|
175
|
+
b = document.create(type: :button, name: :save, scope: second)
|
|
176
|
+
|
|
177
|
+
refute_same a, b
|
|
178
|
+
assert_same a, document.find(:save, scope: first)
|
|
179
|
+
assert_same b, document.find(:save, scope: second)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_each_node_traverses_the_whole_tree_from_root
|
|
183
|
+
document = Teek::UI::Document.new
|
|
184
|
+
a = document.create(type: :button, name: :a)
|
|
185
|
+
b = document.create(type: :column, name: :b)
|
|
186
|
+
c = document.create(type: :button, name: :c)
|
|
187
|
+
document.root.add_child(a)
|
|
188
|
+
document.root.add_child(b)
|
|
189
|
+
b.add_child(c)
|
|
190
|
+
|
|
191
|
+
visited = []
|
|
192
|
+
document.each_node { |n| visited << n }
|
|
193
|
+
|
|
194
|
+
assert_equal [document.root, a, b, c], visited
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_claim_path_segment_returns_the_bare_segment_the_first_time
|
|
198
|
+
document = Teek::UI::Document.new
|
|
199
|
+
|
|
200
|
+
assert_equal 'save', document.claim_path_segment('.list', 'save')
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def test_claim_path_segment_disambiguates_a_repeat_under_the_same_parent
|
|
204
|
+
document = Teek::UI::Document.new
|
|
205
|
+
|
|
206
|
+
document.claim_path_segment('.list', 'save')
|
|
207
|
+
|
|
208
|
+
assert_equal 'save#2', document.claim_path_segment('.list', 'save')
|
|
209
|
+
assert_equal 'save#3', document.claim_path_segment('.list', 'save')
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def test_claim_path_segment_tracks_independently_per_parent_path
|
|
213
|
+
document = Teek::UI::Document.new
|
|
214
|
+
|
|
215
|
+
document.claim_path_segment('.sidebar', 'save')
|
|
216
|
+
|
|
217
|
+
assert_equal 'save', document.claim_path_segment('.main', 'save'),
|
|
218
|
+
"the same segment under a DIFFERENT parent should never need disambiguating"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def test_find_by_path_returns_the_node_whose_realized_path_matches
|
|
222
|
+
document = Teek::UI::Document.new
|
|
223
|
+
node = document.create(type: :button, name: :save)
|
|
224
|
+
document.root.add_child(node)
|
|
225
|
+
node.realized = Teek::UI::RealizedNode.new(app: nil, path: '.win.save')
|
|
226
|
+
|
|
227
|
+
assert_same node, document.find_by_path('.win.save')
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_find_by_path_returns_nil_for_an_unrealized_node
|
|
231
|
+
document = Teek::UI::Document.new
|
|
232
|
+
node = document.create(type: :button, name: :save)
|
|
233
|
+
document.root.add_child(node)
|
|
234
|
+
|
|
235
|
+
assert_nil document.find_by_path('.win.save')
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def test_find_by_path_returns_nil_for_an_unknown_path
|
|
239
|
+
document = Teek::UI::Document.new
|
|
240
|
+
node = document.create(type: :button, name: :save)
|
|
241
|
+
document.root.add_child(node)
|
|
242
|
+
node.realized = Teek::UI::RealizedNode.new(app: nil, path: '.win.save')
|
|
243
|
+
|
|
244
|
+
assert_nil document.find_by_path('.nope')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def test_find_by_path_distinguishes_a_scrollbar_wrapped_nodes_path_from_its_arrange_path
|
|
248
|
+
document = Teek::UI::Document.new
|
|
249
|
+
node = document.create(type: :list, name: :items)
|
|
250
|
+
document.root.add_child(node)
|
|
251
|
+
node.realized = Teek::UI::RealizedNode.new(app: nil, path: '.wrap.items', arrange_path: '.wrap')
|
|
252
|
+
|
|
253
|
+
assert_same node, document.find_by_path('.wrap.items'), "path is the real widget - what a Handle acts on"
|
|
254
|
+
assert_nil document.find_by_path('.wrap'), "arrange_path is only where the parent's layout places it, not this node's own address"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def test_claim_path_segment_persists_across_separate_calls_not_just_one_realizer_instance
|
|
258
|
+
document = Teek::UI::Document.new
|
|
259
|
+
|
|
260
|
+
document.claim_path_segment('.list', 'save')
|
|
261
|
+
# simulates two SEPARATE Realizer instances (e.g. the initial realize,
|
|
262
|
+
# then a later lazily-realized screen sharing the same internal
|
|
263
|
+
# component name) both claiming under the same real parent - the
|
|
264
|
+
# whole reason this lives on Document rather than Realizer itself.
|
|
265
|
+
assert_equal 'save#2', document.claim_path_segment('.list', 'save')
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_add_child_notifies_append_subscribers
|
|
269
|
+
document = Teek::UI::Document.new
|
|
270
|
+
parent = document.create(type: :column, name: :ctrl)
|
|
271
|
+
child = document.create(type: :button)
|
|
272
|
+
seen = []
|
|
273
|
+
document.subscribe(:append) { |p, c| seen << [p, c] }
|
|
274
|
+
|
|
275
|
+
parent.add_child(child)
|
|
276
|
+
|
|
277
|
+
assert_equal [[parent, child]], seen
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def test_add_child_is_silent_with_no_subscribers
|
|
281
|
+
document = Teek::UI::Document.new
|
|
282
|
+
parent = document.create(type: :column)
|
|
283
|
+
child = document.create(type: :button)
|
|
284
|
+
|
|
285
|
+
parent.add_child(child)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def test_notify_reaches_every_subscriber_of_that_event_in_order
|
|
289
|
+
document = Teek::UI::Document.new
|
|
290
|
+
seen = []
|
|
291
|
+
document.subscribe(:push) { |*args| seen << [:first, args] }
|
|
292
|
+
document.subscribe(:push) { |*args| seen << [:second, args] }
|
|
293
|
+
|
|
294
|
+
document.notify(:push, :some_node, 'column')
|
|
295
|
+
|
|
296
|
+
assert_equal [[:first, [:some_node, 'column']], [:second, [:some_node, 'column']]], seen
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def test_notify_only_reaches_subscribers_of_that_specific_event
|
|
300
|
+
document = Teek::UI::Document.new
|
|
301
|
+
push_seen = []
|
|
302
|
+
document.subscribe(:push) { |*args| push_seen << args }
|
|
303
|
+
|
|
304
|
+
document.notify(:pop, :some_node, 'column')
|
|
305
|
+
|
|
306
|
+
assert_equal [], push_seen
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def test_a_raw_node_new_with_no_document_never_calls_notify
|
|
310
|
+
parent = Teek::UI::Node.new(type: :column)
|
|
311
|
+
child = Teek::UI::Node.new(type: :button)
|
|
312
|
+
|
|
313
|
+
parent.add_child(child)
|
|
314
|
+
|
|
315
|
+
assert_equal [child], parent.children
|
|
316
|
+
end
|
|
317
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek/ui/event_bus'
|
|
5
|
+
require 'teek/ui/session'
|
|
6
|
+
|
|
7
|
+
# In-process publish/subscribe for decoupled app events - not Tk events,
|
|
8
|
+
# complementary to on_click/on_key (see Handle). Headless: no Tk/interpreter
|
|
9
|
+
# involved at all, pure Ruby object messaging.
|
|
10
|
+
class TestEventBus < Minitest::Test
|
|
11
|
+
def test_on_and_emit
|
|
12
|
+
bus = Teek::UI::EventBus.new
|
|
13
|
+
received = nil
|
|
14
|
+
|
|
15
|
+
bus.on(:ping) { |val| received = val }
|
|
16
|
+
bus.emit(:ping, 42)
|
|
17
|
+
|
|
18
|
+
assert_equal 42, received
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_emit_with_no_subscribers_is_a_no_op
|
|
22
|
+
bus = Teek::UI::EventBus.new
|
|
23
|
+
|
|
24
|
+
bus.emit(:ghost, 1, 2, 3) # should not raise
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_multiple_subscribers_all_fire_in_subscription_order
|
|
28
|
+
bus = Teek::UI::EventBus.new
|
|
29
|
+
results = []
|
|
30
|
+
|
|
31
|
+
bus.on(:tick) { |v| results << "a:#{v}" }
|
|
32
|
+
bus.on(:tick) { |v| results << "b:#{v}" }
|
|
33
|
+
bus.emit(:tick, 7)
|
|
34
|
+
|
|
35
|
+
assert_equal ['a:7', 'b:7'], results
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_different_events_are_independent
|
|
39
|
+
bus = Teek::UI::EventBus.new
|
|
40
|
+
a = nil
|
|
41
|
+
b = nil
|
|
42
|
+
|
|
43
|
+
bus.on(:foo) { |v| a = v }
|
|
44
|
+
bus.on(:bar) { |v| b = v }
|
|
45
|
+
bus.emit(:foo, 1)
|
|
46
|
+
|
|
47
|
+
assert_equal 1, a
|
|
48
|
+
assert_nil b
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_emit_forwards_multiple_positional_args
|
|
52
|
+
bus = Teek::UI::EventBus.new
|
|
53
|
+
received = nil
|
|
54
|
+
|
|
55
|
+
bus.on(:multi) { |x, y| received = [x, y] }
|
|
56
|
+
bus.emit(:multi, :a, :b)
|
|
57
|
+
|
|
58
|
+
assert_equal [:a, :b], received
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_emit_forwards_keyword_args
|
|
62
|
+
bus = Teek::UI::EventBus.new
|
|
63
|
+
received = nil
|
|
64
|
+
|
|
65
|
+
bus.on(:kw) { |name:, val:| received = { name: name, val: val } }
|
|
66
|
+
bus.emit(:kw, name: 'scale', val: 3)
|
|
67
|
+
|
|
68
|
+
assert_equal({ name: 'scale', val: 3 }, received)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_off_removes_a_specific_subscriber
|
|
72
|
+
bus = Teek::UI::EventBus.new
|
|
73
|
+
received = []
|
|
74
|
+
|
|
75
|
+
block = bus.on(:evt) { |v| received << v }
|
|
76
|
+
bus.emit(:evt, 1)
|
|
77
|
+
bus.off(:evt, block)
|
|
78
|
+
bus.emit(:evt, 2)
|
|
79
|
+
|
|
80
|
+
assert_equal [1], received
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_off_only_removes_the_given_subscriber_not_others_on_the_same_event
|
|
84
|
+
bus = Teek::UI::EventBus.new
|
|
85
|
+
received = []
|
|
86
|
+
|
|
87
|
+
keep = bus.on(:evt) { |v| received << "keep:#{v}" }
|
|
88
|
+
drop = bus.on(:evt) { |v| received << "drop:#{v}" }
|
|
89
|
+
bus.off(:evt, drop)
|
|
90
|
+
bus.emit(:evt, 1)
|
|
91
|
+
|
|
92
|
+
assert_equal ['keep:1'], received
|
|
93
|
+
refute_nil keep
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_on_returns_the_block_for_a_later_off
|
|
97
|
+
bus = Teek::UI::EventBus.new
|
|
98
|
+
|
|
99
|
+
block = bus.on(:x) { }
|
|
100
|
+
|
|
101
|
+
assert_instance_of Proc, block
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_two_independent_buses_do_not_see_each_others_events
|
|
105
|
+
bus_a = Teek::UI::EventBus.new
|
|
106
|
+
bus_b = Teek::UI::EventBus.new
|
|
107
|
+
received_a = nil
|
|
108
|
+
received_b = nil
|
|
109
|
+
|
|
110
|
+
bus_a.on(:shared_name) { |v| received_a = v }
|
|
111
|
+
bus_b.on(:shared_name) { |v| received_b = v }
|
|
112
|
+
bus_a.emit(:shared_name, 'from a')
|
|
113
|
+
|
|
114
|
+
assert_equal 'from a', received_a
|
|
115
|
+
assert_nil received_b, "bus_b should never see bus_a's emit"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Session#on/#emit/#off delegate to one EventBus per session - app-scoped,
|
|
120
|
+
# not a global singleton, so two Teek::UI.app instances in the same
|
|
121
|
+
# process never see each other's events. Headless: works before realize,
|
|
122
|
+
# no Tk/interpreter needed at all.
|
|
123
|
+
class TestSessionEventBus < Minitest::Test
|
|
124
|
+
def build_session
|
|
125
|
+
Teek::UI::Session.new(title: 'Event Bus Test')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_on_and_emit_work_before_realize
|
|
129
|
+
session = build_session
|
|
130
|
+
received = nil
|
|
131
|
+
|
|
132
|
+
session.on(:item_selected) { |id| received = id }
|
|
133
|
+
session.emit(:item_selected, 42)
|
|
134
|
+
|
|
135
|
+
assert_equal 42, received
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_off_removes_a_specific_subscriber
|
|
139
|
+
session = build_session
|
|
140
|
+
received = []
|
|
141
|
+
|
|
142
|
+
block = session.on(:evt) { |v| received << v }
|
|
143
|
+
session.emit(:evt, 1)
|
|
144
|
+
session.off(:evt, block)
|
|
145
|
+
session.emit(:evt, 2)
|
|
146
|
+
|
|
147
|
+
assert_equal [1], received
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_two_independent_sessions_do_not_see_each_others_events
|
|
151
|
+
session_a = build_session
|
|
152
|
+
session_b = build_session
|
|
153
|
+
received_a = nil
|
|
154
|
+
received_b = nil
|
|
155
|
+
|
|
156
|
+
session_a.on(:theme_changed) { |v| received_a = v }
|
|
157
|
+
session_b.on(:theme_changed) { |v| received_b = v }
|
|
158
|
+
session_a.emit(:theme_changed, 'dark')
|
|
159
|
+
|
|
160
|
+
assert_equal 'dark', received_a
|
|
161
|
+
assert_nil received_b, "session_b should never see session_a's emit"
|
|
162
|
+
end
|
|
163
|
+
end
|
data/test/test_events.rb
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
class TestEvents < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_on_click_fires_on_a_real_button_event
|
|
10
|
+
assert_tk_app("on_click should fire when Button-1 is generated on the widget") do
|
|
11
|
+
require 'teek/ui'
|
|
12
|
+
|
|
13
|
+
clicked = false
|
|
14
|
+
session = Teek::UI.app(title: 'Events Test') { |ui| ui.button(:go, text: 'Go') }
|
|
15
|
+
session.run_async
|
|
16
|
+
session.app.update
|
|
17
|
+
session[:go].on_click { clicked = true }
|
|
18
|
+
|
|
19
|
+
session.app.tcl_eval("event generate #{session[:go].path} <Button-1>")
|
|
20
|
+
session.app.update
|
|
21
|
+
|
|
22
|
+
assert clicked, "on_click did not fire"
|
|
23
|
+
|
|
24
|
+
session.app.destroy
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_on_right_click_fires_on_the_linux_windows_right_click_event
|
|
29
|
+
assert_tk_app("on_right_click should fire on Button-3 (Linux/Windows right-click)") do
|
|
30
|
+
require 'teek/ui'
|
|
31
|
+
|
|
32
|
+
clicked = false
|
|
33
|
+
session = Teek::UI.app(title: 'Events Test') { |ui| ui.button(:go, text: 'Go') }
|
|
34
|
+
session.run_async
|
|
35
|
+
session.app.update
|
|
36
|
+
session[:go].on_right_click { clicked = true }
|
|
37
|
+
|
|
38
|
+
session.app.tcl_eval("event generate #{session[:go].path} <Button-3>")
|
|
39
|
+
session.app.update
|
|
40
|
+
|
|
41
|
+
assert clicked, "on_right_click did not fire on Button-3"
|
|
42
|
+
|
|
43
|
+
session.app.destroy
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_on_drag_delivers_typed_integer_coordinates
|
|
48
|
+
assert_tk_app("on_drag should deliver Integer x/y, not raw Tcl strings") do
|
|
49
|
+
require 'teek/ui'
|
|
50
|
+
|
|
51
|
+
received = nil
|
|
52
|
+
session = Teek::UI.app(title: 'Events Test') { |ui| ui.panel(:area, width: 200, height: 200) }
|
|
53
|
+
session.run_async
|
|
54
|
+
session.app.update
|
|
55
|
+
session[:area].on_drag { |x, y| received = [x, y] }
|
|
56
|
+
|
|
57
|
+
session.app.tcl_eval("event generate #{session[:area].path} <B1-Motion> -x 40 -y 55")
|
|
58
|
+
session.app.update
|
|
59
|
+
|
|
60
|
+
assert_equal [40, 55], received
|
|
61
|
+
assert_kind_of Integer, received[0]
|
|
62
|
+
assert_kind_of Integer, received[1]
|
|
63
|
+
|
|
64
|
+
session.app.destroy
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_on_drag_converts_coordinates_when_bound_to_a_canvas
|
|
69
|
+
assert_tk_app("on_drag should convert raw window coords through canvasx/canvasy when bound to a canvas") do
|
|
70
|
+
require 'teek/ui'
|
|
71
|
+
|
|
72
|
+
received = nil
|
|
73
|
+
session = Teek::UI.app(title: 'Events Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
74
|
+
session.run_async
|
|
75
|
+
session.app.update
|
|
76
|
+
|
|
77
|
+
board_path = session[:board].path
|
|
78
|
+
# scroll the canvas so canvasx/canvasy meaningfully differ from raw %x/%y -
|
|
79
|
+
# otherwise a bug that skips conversion entirely could still pass by luck.
|
|
80
|
+
session.app.tcl_eval("#{board_path} configure -scrollregion {0 0 1000 1000}")
|
|
81
|
+
session.app.tcl_eval("#{board_path} xview moveto 0.5")
|
|
82
|
+
session.app.update
|
|
83
|
+
|
|
84
|
+
session[:board].on_drag { |x, y| received = [x, y] }
|
|
85
|
+
session.app.tcl_eval("event generate #{board_path} <B1-Motion> -x 50 -y 60")
|
|
86
|
+
session.app.update
|
|
87
|
+
|
|
88
|
+
expected_x = session.app.command(board_path, :canvasx, 50).to_f.round
|
|
89
|
+
expected_y = session.app.command(board_path, :canvasy, 60).to_f.round
|
|
90
|
+
|
|
91
|
+
refute_nil received
|
|
92
|
+
assert_equal [expected_x, expected_y], received
|
|
93
|
+
refute_equal [50, 60], received, "the canvas is scrolled, so conversion should actually change the coordinates"
|
|
94
|
+
|
|
95
|
+
session.app.destroy
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_on_key_resolves_a_friendly_symbol
|
|
100
|
+
assert_tk_app("on_key(:enter) should fire on a real Return keypress") do
|
|
101
|
+
require 'teek/ui'
|
|
102
|
+
|
|
103
|
+
fired = false
|
|
104
|
+
session = Teek::UI.app(title: 'Events Test') { |ui| ui.text_box(:query) }
|
|
105
|
+
session.run_async
|
|
106
|
+
session.app.update
|
|
107
|
+
session[:query].on_key(:enter) { fired = true }
|
|
108
|
+
|
|
109
|
+
path = session[:query].path
|
|
110
|
+
session.app.tcl_eval("focus -force #{path}")
|
|
111
|
+
session.app.update
|
|
112
|
+
session.app.tcl_eval("event generate #{path} <Return>")
|
|
113
|
+
session.app.update
|
|
114
|
+
|
|
115
|
+
assert fired, "on_key(:enter) did not fire"
|
|
116
|
+
|
|
117
|
+
session.app.destroy
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_on_key_resolves_a_ctrl_modifier_string
|
|
122
|
+
assert_tk_app("on_key('Ctrl-s') should fire on a real Control-s keypress") do
|
|
123
|
+
require 'teek/ui'
|
|
124
|
+
|
|
125
|
+
fired = false
|
|
126
|
+
session = Teek::UI.app(title: 'Events Test') { |ui| ui.text_box(:query) }
|
|
127
|
+
session.run_async
|
|
128
|
+
session.app.update
|
|
129
|
+
session[:query].on_key('Ctrl-s') { fired = true }
|
|
130
|
+
|
|
131
|
+
path = session[:query].path
|
|
132
|
+
session.app.tcl_eval("focus -force #{path}")
|
|
133
|
+
session.app.update
|
|
134
|
+
session.app.tcl_eval("event generate #{path} <Control-s>")
|
|
135
|
+
session.app.update
|
|
136
|
+
|
|
137
|
+
assert fired, "on_key('Ctrl-s') did not fire"
|
|
138
|
+
|
|
139
|
+
session.app.destroy
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_on_key_shift_tab_fires_via_the_iso_left_tab_gotcha
|
|
144
|
+
assert_tk_app("on_key('Shift-Tab') should fire even though X11 delivers it as ISO_Left_Tab, not Shift-Tab") do
|
|
145
|
+
require 'teek/ui'
|
|
146
|
+
|
|
147
|
+
fired = false
|
|
148
|
+
session = Teek::UI.app(title: 'Events Test') { |ui| ui.text_box(:query) }
|
|
149
|
+
session.run_async
|
|
150
|
+
session.app.update
|
|
151
|
+
session[:query].on_key('Shift-Tab') { fired = true }
|
|
152
|
+
|
|
153
|
+
path = session[:query].path
|
|
154
|
+
session.app.tcl_eval("focus -force #{path}")
|
|
155
|
+
session.app.update
|
|
156
|
+
session.app.tcl_eval("event generate #{path} <ISO_Left_Tab>")
|
|
157
|
+
session.app.update
|
|
158
|
+
|
|
159
|
+
assert fired, "on_key('Shift-Tab') did not fire for the X11 ISO_Left_Tab keysym"
|
|
160
|
+
|
|
161
|
+
session.app.destroy
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek'
|
|
5
|
+
|
|
6
|
+
# support/fake_app.rb's FakeApp/FakeWindow stand in for Teek::App/
|
|
7
|
+
# Teek::Window in headless tests - Realizer/Handle only ever call a
|
|
8
|
+
# handful of methods on whichever app object they're given, so the fakes
|
|
9
|
+
# implement just those, not the real classes' full surface. What they DO
|
|
10
|
+
# implement needs to stay call-compatible with the real thing, or a
|
|
11
|
+
# headless test could keep passing against a contract production code no
|
|
12
|
+
# longer honors. RSpec has verifying doubles (instance_double) for
|
|
13
|
+
# exactly this; Minitest doesn't, so this hand-rolls the same idea as an
|
|
14
|
+
# ordinary failing test instead of a load-time check that would just
|
|
15
|
+
# blow up the whole suite with a raw exception.
|
|
16
|
+
class TestFakeAppContract < Minitest::Test
|
|
17
|
+
FAKE_APP_METHODS = %i[command bind on_close popup_menu window].freeze
|
|
18
|
+
FAKE_WINDOW_METHODS = %i[modal grab_release].freeze
|
|
19
|
+
|
|
20
|
+
def test_fake_app_methods_are_signature_compatible_with_teek_app
|
|
21
|
+
FAKE_APP_METHODS.each do |name|
|
|
22
|
+
assert_signature_compatible(FakeApp.instance_method(name), Teek::App.instance_method(name), name)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_fake_window_methods_are_signature_compatible_with_teek_window
|
|
27
|
+
FAKE_WINDOW_METHODS.each do |name|
|
|
28
|
+
assert_signature_compatible(FakeWindow.instance_method(name), Teek::Window.instance_method(name), name)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Keyword names must match exactly (a renamed/added/removed kwarg is
|
|
35
|
+
# the drift most likely to silently break a fake) - required vs
|
|
36
|
+
# optional isn't distinguished, since the fake defaulting a kwarg the
|
|
37
|
+
# real method requires is a harmless, common simplification for a stub.
|
|
38
|
+
# Positional/rest SHAPE (ignoring req vs opt, same reasoning) must also
|
|
39
|
+
# line up, since a dropped or added *args changes what call sites the
|
|
40
|
+
# fake can stand in for. +&block+ presence is deliberately NOT compared
|
|
41
|
+
# - a real method can accept a block via bare +yield+/+block_given?+
|
|
42
|
+
# with no +&block+ param at all (see Teek::Window#modal), which
|
|
43
|
+
# +Method#parameters+ has no way to see - only a fake DECLARING a block
|
|
44
|
+
# param when the real method can't take one at all would be a genuine
|
|
45
|
+
# mismatch, and that's not a failure mode worth the false positives
|
|
46
|
+
# this check would otherwise raise on every yield-based real method.
|
|
47
|
+
def assert_signature_compatible(fake_method, real_method, name)
|
|
48
|
+
assert_equal keyword_names(real_method), keyword_names(fake_method),
|
|
49
|
+
"FakeApp/FakeWindow##{name}'s keyword arguments have drifted from the real Teek method's"
|
|
50
|
+
|
|
51
|
+
assert_equal positional_shape(real_method), positional_shape(fake_method),
|
|
52
|
+
"FakeApp/FakeWindow##{name}'s positional/rest shape has drifted from the real Teek method's"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def keyword_names(method)
|
|
56
|
+
method.parameters.select { |kind, _| %i[key keyreq].include?(kind) }.map { |_, param_name| param_name }.sort
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def positional_shape(method)
|
|
60
|
+
method.parameters.reject { |kind, _| %i[key keyreq keyrest block].include?(kind) }.map { |kind, _| kind }
|
|
61
|
+
end
|
|
62
|
+
end
|