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,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# Real-Tk coverage of Handle#destroy!'s auto-defer behavior - needs a
|
|
7
|
+
# genuine Tcl callback dispatch to exercise (Teek.in_callback? reflects
|
|
8
|
+
# the real interpreter's live callback depth, meaningless without one),
|
|
9
|
+
# so this can't be headless like test_lazy_realize.rb's Realizer-level
|
|
10
|
+
# coverage.
|
|
11
|
+
class TestHandleDestroyRealTk < Minitest::Test
|
|
12
|
+
include TeekTestHelper
|
|
13
|
+
|
|
14
|
+
def test_a_close_button_destroying_its_own_containing_window_does_not_error
|
|
15
|
+
assert_tk_app("a close button tearing down its own containing window via destroy! should not race ttk's own internal same-click bindings") do
|
|
16
|
+
require 'teek/ui'
|
|
17
|
+
|
|
18
|
+
handle = nil
|
|
19
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
20
|
+
handle = ui.window(:dialog, modal: true) { |w| w.button(:close, text: 'Close').on_click { handle.destroy! } }
|
|
21
|
+
end
|
|
22
|
+
session.run_async
|
|
23
|
+
session.app.update
|
|
24
|
+
|
|
25
|
+
# the "invalid command name" hazard this fixes only ever surfaced
|
|
26
|
+
# via Tcl's bgerror (a background/asynchronous error), never as a
|
|
27
|
+
# Ruby exception raised back through event generate - override it
|
|
28
|
+
# to capture the message instead of letting the default handler
|
|
29
|
+
# print/ignore it.
|
|
30
|
+
session.app.tcl_eval(<<~'TCL')
|
|
31
|
+
proc _test_bgerror {msg opts} {
|
|
32
|
+
set ::bgerror_msg $msg
|
|
33
|
+
}
|
|
34
|
+
interp bgerror {} _test_bgerror
|
|
35
|
+
set ::bgerror_msg {}
|
|
36
|
+
TCL
|
|
37
|
+
|
|
38
|
+
session.app.tcl_eval("event generate #{handle.path}.close <Button-1>")
|
|
39
|
+
session.app.tcl_eval("event generate #{handle.path}.close <ButtonRelease-1>")
|
|
40
|
+
session.app.update
|
|
41
|
+
|
|
42
|
+
bgerror = session.app.tcl_eval('set ::bgerror_msg')
|
|
43
|
+
assert_empty bgerror, "no Tcl-level error should have been raised by ttk's own bindings running against an already-destroyed widget"
|
|
44
|
+
|
|
45
|
+
session.app.destroy
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_destroy_outside_any_callback_is_synchronous
|
|
50
|
+
assert_tk_app("destroy! called outside a callback (default defer: nil) should tear down immediately, with no update needed") do
|
|
51
|
+
require 'teek/ui'
|
|
52
|
+
|
|
53
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:box) }
|
|
54
|
+
session.run_async
|
|
55
|
+
session.app.update
|
|
56
|
+
|
|
57
|
+
path = session[:box].path
|
|
58
|
+
session[:box].destroy!
|
|
59
|
+
|
|
60
|
+
refute session.app.winfo.exists?(path), "destroy! outside a callback should destroy synchronously, before the call returns"
|
|
61
|
+
|
|
62
|
+
session.app.destroy
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_defer_false_forces_synchronous_destroy_even_inside_a_callback
|
|
67
|
+
assert_tk_app("destroy!(defer: false) should force an immediate synchronous destroy even from inside a callback") do
|
|
68
|
+
require 'teek/ui'
|
|
69
|
+
|
|
70
|
+
handle = nil
|
|
71
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
72
|
+
ui.panel(:host) do |p|
|
|
73
|
+
handle = p.panel(:box)
|
|
74
|
+
p.button(:go, text: 'Go').on_click { handle.destroy!(defer: false) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
session.run_async
|
|
78
|
+
session.app.update
|
|
79
|
+
path = handle.path
|
|
80
|
+
|
|
81
|
+
session.app.tcl_eval("event generate #{session[:go].path} <Button-1>")
|
|
82
|
+
# deliberately NOT calling session.app.update before checking - a
|
|
83
|
+
# forced synchronous destroy should already be gone, without
|
|
84
|
+
# needing an idle pass to catch up.
|
|
85
|
+
refute session.app.winfo.exists?(path)
|
|
86
|
+
|
|
87
|
+
session.app.destroy
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_defer_true_forces_deferred_destroy_even_outside_a_callback
|
|
92
|
+
assert_tk_app("destroy!(defer: true) should defer even outside a callback - the widget is still there right after the call, gone after the next idle") do
|
|
93
|
+
require 'teek/ui'
|
|
94
|
+
|
|
95
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:box) }
|
|
96
|
+
session.run_async
|
|
97
|
+
session.app.update
|
|
98
|
+
|
|
99
|
+
path = session[:box].path
|
|
100
|
+
session[:box].destroy!(defer: true)
|
|
101
|
+
|
|
102
|
+
assert session.app.winfo.exists?(path), "a deferred destroy should not have run yet, synchronously, right after the call"
|
|
103
|
+
|
|
104
|
+
session.app.update
|
|
105
|
+
|
|
106
|
+
refute session.app.winfo.exists?(path), "the deferred destroy should have run by the next update (which processes idle callbacks)"
|
|
107
|
+
|
|
108
|
+
session.app.destroy
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_calling_destroy_twice_while_a_deferred_destroy_is_still_pending_is_safe
|
|
113
|
+
assert_tk_app("calling destroy! a second time on the same handle before its own deferred teardown has run should not raise or double-schedule") do
|
|
114
|
+
require 'teek/ui'
|
|
115
|
+
|
|
116
|
+
handle = nil
|
|
117
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
118
|
+
ui.panel(:host) do |p|
|
|
119
|
+
handle = p.panel(:box)
|
|
120
|
+
p.button(:go, text: 'Go').on_click {
|
|
121
|
+
handle.destroy!
|
|
122
|
+
handle.destroy! # same handle, still pending - must not raise
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
session.run_async
|
|
127
|
+
session.app.update
|
|
128
|
+
path = handle.path
|
|
129
|
+
|
|
130
|
+
session.app.tcl_eval("event generate #{session[:go].path} <Button-1>")
|
|
131
|
+
session.app.update
|
|
132
|
+
|
|
133
|
+
refute session.app.winfo.exists?(path)
|
|
134
|
+
|
|
135
|
+
session.app.destroy
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def test_destroying_an_ancestor_then_a_descendant_is_safe
|
|
140
|
+
assert_tk_app("destroying an ancestor's handle, then a descendant's own handle, should not raise even though the descendant's own Tk widget is already gone") do
|
|
141
|
+
require 'teek/ui'
|
|
142
|
+
|
|
143
|
+
parent_handle = nil
|
|
144
|
+
child_handle = nil
|
|
145
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
146
|
+
ui.panel(:host) do |p|
|
|
147
|
+
parent_handle = p.panel(:box) { |b| child_handle = b.button(:inner, text: 'Inner') }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
session.run_async
|
|
151
|
+
session.app.update
|
|
152
|
+
|
|
153
|
+
parent_handle.destroy!(defer: false)
|
|
154
|
+
# the descendant's OWN Node#realized is untouched by the ancestor's
|
|
155
|
+
# destroy (only the ancestor's own node gets reset) - its Tk widget
|
|
156
|
+
# is gone already, but Tcl's own destroy command is a documented
|
|
157
|
+
# no-op on an already-gone path, so this should not raise.
|
|
158
|
+
child_handle.destroy!(defer: false)
|
|
159
|
+
|
|
160
|
+
session.app.destroy
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_a_deferred_destroy_releases_its_callback_by_the_next_update
|
|
165
|
+
assert_tk_app("a deferred destroy should release its widget's callback once the idle pass runs, same as a synchronous destroy does immediately") do
|
|
166
|
+
require 'teek/ui'
|
|
167
|
+
|
|
168
|
+
handle = nil
|
|
169
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
170
|
+
ui.panel(:host) do |p|
|
|
171
|
+
handle = p.panel(:box) { |b| b.button(:a, text: 'A').on_click { } }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
session.run_async
|
|
175
|
+
session.app.update
|
|
176
|
+
with_button_baseline = session.app.interp.callback_ids.length
|
|
177
|
+
|
|
178
|
+
handle.destroy!(defer: true)
|
|
179
|
+
# scheduling the deferred teardown itself registers one new
|
|
180
|
+
# after_idle callback - :a's own on_click isn't released yet.
|
|
181
|
+
assert_equal with_button_baseline + 1, session.app.interp.callback_ids.length,
|
|
182
|
+
"a still-pending deferred destroy should not have released :a's callback yet"
|
|
183
|
+
|
|
184
|
+
session.app.update
|
|
185
|
+
|
|
186
|
+
assert_equal with_button_baseline - 1, session.app.interp.callback_ids.length,
|
|
187
|
+
"once the deferred destroy actually runs, both its own after_idle registration and :a's on_click should be released"
|
|
188
|
+
|
|
189
|
+
session.app.destroy
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_destroying_a_widget_unlinks_it_from_its_parents_children
|
|
194
|
+
assert_tk_app("destroy! should remove the destroyed node from its parent's own children, not just clear its Tk-realized state") do
|
|
195
|
+
require 'teek/ui'
|
|
196
|
+
|
|
197
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
198
|
+
ui.panel(:host) { |p| p.button(:item, text: 'Item') }
|
|
199
|
+
end
|
|
200
|
+
session.run_async
|
|
201
|
+
session.app.update
|
|
202
|
+
host_node = session.document.find(:host)
|
|
203
|
+
item_node = session.document.find(:item)
|
|
204
|
+
|
|
205
|
+
session[:item].destroy!(defer: false)
|
|
206
|
+
|
|
207
|
+
refute_includes host_node.children, item_node
|
|
208
|
+
|
|
209
|
+
session.app.destroy
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def test_destroying_a_named_widget_frees_its_name_for_reuse
|
|
214
|
+
assert_tk_app("destroying a named widget should let a later widget reuse the same name in the same scope, and ui[:name] should be nil in between") do
|
|
215
|
+
require 'teek/ui'
|
|
216
|
+
|
|
217
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:host) }
|
|
218
|
+
session.run_async
|
|
219
|
+
session.app.update
|
|
220
|
+
|
|
221
|
+
session.add(:host) { |a| a.button(:item, text: 'First') }
|
|
222
|
+
session.app.update
|
|
223
|
+
first_path = session[:item].path
|
|
224
|
+
session[:item].destroy!(defer: false)
|
|
225
|
+
|
|
226
|
+
assert_nil session[:item], "ui[:item] should be nil once the widget that owned that name is destroyed"
|
|
227
|
+
|
|
228
|
+
session.add(:host) { |a| a.button(:item, text: 'Second') }
|
|
229
|
+
session.app.update
|
|
230
|
+
|
|
231
|
+
refute_nil session[:item], "a new :item should be addable again under the same name once the old one is gone"
|
|
232
|
+
assert_equal 'Second', session.app.command(session[:item].path, :cget, '-text')
|
|
233
|
+
refute_equal first_path, session[:item].path
|
|
234
|
+
|
|
235
|
+
session.app.destroy
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def test_repeated_add_and_destroy_of_a_named_widget_under_one_parent_does_not_crash
|
|
240
|
+
assert_tk_app("repeatedly adding and destroying a plain (non-window) named widget under one shared parent should never crash arrange_children") do
|
|
241
|
+
require 'teek/ui'
|
|
242
|
+
|
|
243
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:host) }
|
|
244
|
+
session.run_async
|
|
245
|
+
session.app.update
|
|
246
|
+
|
|
247
|
+
5.times do |i|
|
|
248
|
+
handle = nil
|
|
249
|
+
session.add(:host) { |a| handle = a.button(:item, text: "Item #{i}") }
|
|
250
|
+
session.app.update
|
|
251
|
+
assert_equal "Item #{i}", session.app.command(handle.path, :cget, '-text')
|
|
252
|
+
handle.destroy!(defer: false)
|
|
253
|
+
session.app.update
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
assert_nil session[:item]
|
|
257
|
+
assert_equal 0, session.document.find(:host).children.length
|
|
258
|
+
|
|
259
|
+
session.app.destroy
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_a_sibling_added_after_a_destroy_still_arranges_correctly
|
|
264
|
+
assert_tk_app("arrange_children should correctly position a new sibling relative to survivors, with no trace of a destroyed sibling") do
|
|
265
|
+
require 'teek/ui'
|
|
266
|
+
|
|
267
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
268
|
+
ui.column(:host, gap: 10) { |c| c.button(:first, text: 'First') }
|
|
269
|
+
end
|
|
270
|
+
session.run_async
|
|
271
|
+
session.app.update
|
|
272
|
+
|
|
273
|
+
doomed = nil
|
|
274
|
+
session.add(:host) { |a| doomed = a.button(:doomed, text: 'Doomed') }
|
|
275
|
+
session.app.update
|
|
276
|
+
doomed.destroy!(defer: false)
|
|
277
|
+
session.app.update
|
|
278
|
+
|
|
279
|
+
session.add(:host) { |a| a.button(:second, text: 'Second') }
|
|
280
|
+
session.app.update
|
|
281
|
+
|
|
282
|
+
first_bottom = session.app.winfo.rooty(session[:first].path) + session.app.winfo.height(session[:first].path)
|
|
283
|
+
second_top = session.app.winfo.rooty(session[:second].path)
|
|
284
|
+
assert_equal 10, second_top - first_bottom,
|
|
285
|
+
"the new sibling should be positioned relative to :first, as if :doomed never existed"
|
|
286
|
+
|
|
287
|
+
session.app.destroy
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def test_popping_and_destroying_a_screen_leaves_no_stale_entry_in_screens
|
|
292
|
+
assert_tk_app("Screens#pop already removes its own Entry before returning the handle, so destroy! afterward can't leave anything stale in Screens' own bookkeeping") do
|
|
293
|
+
require 'teek/ui'
|
|
294
|
+
|
|
295
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:picker) }
|
|
296
|
+
session.run_async
|
|
297
|
+
session.app.update
|
|
298
|
+
|
|
299
|
+
session.screens.push(:picker, session[:picker])
|
|
300
|
+
popped = session.screens.pop
|
|
301
|
+
popped.destroy!(defer: false)
|
|
302
|
+
|
|
303
|
+
refute session.screens.active?
|
|
304
|
+
assert_nil session.screens.current
|
|
305
|
+
assert_nil session.screens.current_screen
|
|
306
|
+
assert_equal 0, session.screens.size
|
|
307
|
+
|
|
308
|
+
session.app.destroy
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_destroying_an_unrelated_widget_does_not_disturb_an_already_built_menu
|
|
313
|
+
assert_tk_app("destroying (and unlinking) an unrelated widget elsewhere in the tree should have zero effect on an already-built menu's own entries/ordering") do
|
|
314
|
+
require 'teek/ui'
|
|
315
|
+
|
|
316
|
+
clicked = []
|
|
317
|
+
session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
|
|
318
|
+
ui.menu_bar do |mb|
|
|
319
|
+
mb.menu(:file, label: 'File') do |f|
|
|
320
|
+
f.item(label: 'One') { clicked << :one }
|
|
321
|
+
f.item(label: 'Two') { clicked << :two }
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
ui.panel(:host) { |p| p.button(:item, text: 'Item') }
|
|
325
|
+
end
|
|
326
|
+
session.run_async
|
|
327
|
+
session.app.update
|
|
328
|
+
|
|
329
|
+
session[:item].destroy!(defer: false)
|
|
330
|
+
session.app.update
|
|
331
|
+
|
|
332
|
+
file_path = session[:file].path
|
|
333
|
+
assert_equal 'One', session.app.command(file_path, :entrycget, 0, '-label')
|
|
334
|
+
assert_equal 'Two', session.app.command(file_path, :entrycget, 1, '-label')
|
|
335
|
+
|
|
336
|
+
session.app.tcl_eval("#{file_path} invoke 0")
|
|
337
|
+
session.app.tcl_eval("#{file_path} invoke 1")
|
|
338
|
+
|
|
339
|
+
assert_equal [:one, :two], clicked, "menu entries should still fire in their original order, unaffected by an unrelated destroy elsewhere"
|
|
340
|
+
|
|
341
|
+
session.app.destroy
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if ENV['COVERAGE']
|
|
4
|
+
require 'simplecov'
|
|
5
|
+
require_relative '../../test/simplecov_config'
|
|
6
|
+
|
|
7
|
+
coverage_name = ENV['COVERAGE_NAME'] || 'ui'
|
|
8
|
+
SimpleCov.coverage_dir "#{SimpleCovConfig::PROJECT_ROOT}/coverage/results/#{coverage_name}"
|
|
9
|
+
SimpleCov.command_name "ui:#{coverage_name}"
|
|
10
|
+
SimpleCov.print_error_status = false
|
|
11
|
+
SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
|
|
12
|
+
|
|
13
|
+
SimpleCov.start do
|
|
14
|
+
SimpleCovConfig.apply_filters(self)
|
|
15
|
+
track_files "#{SimpleCovConfig::PROJECT_ROOT}/lib/**/*.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require "minitest/autorun"
|
|
20
|
+
require_relative "support/fake_app"
|
data/test/test_image.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek/ui/image'
|
|
5
|
+
|
|
6
|
+
class TestImage < Minitest::Test
|
|
7
|
+
def test_name_is_the_allocated_tcl_image_name
|
|
8
|
+
image = Teek::UI::Image.new('teek_ui_image_1', '/tmp/whatever.png', {})
|
|
9
|
+
|
|
10
|
+
assert_equal 'teek_ui_image_1', image.name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_to_s_is_the_allocated_tcl_image_name
|
|
14
|
+
image = Teek::UI::Image.new('teek_ui_image_1', '/tmp/whatever.png', {})
|
|
15
|
+
|
|
16
|
+
assert_equal 'teek_ui_image_1', image.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_photo_raises_before_realize
|
|
20
|
+
image = Teek::UI::Image.new('teek_ui_image_1', '/tmp/whatever.png', {})
|
|
21
|
+
|
|
22
|
+
assert_raises(Teek::UI::NotRealizedError) { image.photo }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# Real-Tk half of image support's coverage - Image's own build-time shape
|
|
7
|
+
# (name allocation, raises before realize) is covered headlessly in
|
|
8
|
+
# test_image.rb; these exercise the actual load-and-display path against
|
|
9
|
+
# a real Tk photo image.
|
|
10
|
+
class TestImageRealTk < Minitest::Test
|
|
11
|
+
include TeekTestHelper
|
|
12
|
+
|
|
13
|
+
def test_a_label_shows_an_image_loaded_via_the_dsl
|
|
14
|
+
assert_tk_app("ui.image(path) + image: on a label should load and display a real Tk photo image") do
|
|
15
|
+
require 'teek/ui'
|
|
16
|
+
require 'tmpdir'
|
|
17
|
+
|
|
18
|
+
Dir.mktmpdir do |dir|
|
|
19
|
+
path = File.join(dir, 'test.png')
|
|
20
|
+
seed = Teek::Photo.new(app, width: 8, height: 8)
|
|
21
|
+
red = ([255, 0, 0, 255].pack('CCCC')) * (8 * 8)
|
|
22
|
+
seed.put_block(red, 8, 8)
|
|
23
|
+
app.tcl_eval("#{seed.name} write {#{path}} -format png")
|
|
24
|
+
seed.delete
|
|
25
|
+
|
|
26
|
+
icon = nil
|
|
27
|
+
session = Teek::UI.app(title: 'Image Test') do |ui|
|
|
28
|
+
icon = ui.image(path)
|
|
29
|
+
ui.label(:pic, image: icon)
|
|
30
|
+
end
|
|
31
|
+
session.run_async
|
|
32
|
+
session.app.update
|
|
33
|
+
|
|
34
|
+
pic_path = session[:pic].path
|
|
35
|
+
assert_equal icon.name, session.app.command(pic_path, :cget, '-image')
|
|
36
|
+
assert_equal 'photo', session.app.tcl_eval("image type #{icon.name}")
|
|
37
|
+
w, h = icon.photo.get_size
|
|
38
|
+
assert_equal 8, w
|
|
39
|
+
assert_equal 8, h
|
|
40
|
+
|
|
41
|
+
session.app.destroy
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_a_button_shows_an_image_loaded_via_the_dsl
|
|
47
|
+
assert_tk_app("ui.image(path) + image: on a button should load and display a real Tk photo image") do
|
|
48
|
+
require 'teek/ui'
|
|
49
|
+
require 'tmpdir'
|
|
50
|
+
|
|
51
|
+
Dir.mktmpdir do |dir|
|
|
52
|
+
path = File.join(dir, 'test.png')
|
|
53
|
+
seed = Teek::Photo.new(app, width: 8, height: 8)
|
|
54
|
+
blue = ([0, 0, 255, 255].pack('CCCC')) * (8 * 8)
|
|
55
|
+
seed.put_block(blue, 8, 8)
|
|
56
|
+
app.tcl_eval("#{seed.name} write {#{path}} -format png")
|
|
57
|
+
seed.delete
|
|
58
|
+
|
|
59
|
+
icon = nil
|
|
60
|
+
session = Teek::UI.app(title: 'Image Test') do |ui|
|
|
61
|
+
icon = ui.image(path)
|
|
62
|
+
ui.button(:go, image: icon)
|
|
63
|
+
end
|
|
64
|
+
session.run_async
|
|
65
|
+
session.app.update
|
|
66
|
+
|
|
67
|
+
assert_equal icon.name, session.app.command(session[:go].path, :cget, '-image')
|
|
68
|
+
|
|
69
|
+
session.app.destroy
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_replacing_a_widgets_image_via_configure_works
|
|
75
|
+
assert_tk_app("handle.configure(image: another_image) should swap the displayed image") do
|
|
76
|
+
require 'teek/ui'
|
|
77
|
+
require 'tmpdir'
|
|
78
|
+
|
|
79
|
+
Dir.mktmpdir do |dir|
|
|
80
|
+
path_a = File.join(dir, 'a.png')
|
|
81
|
+
path_b = File.join(dir, 'b.png')
|
|
82
|
+
[path_a, path_b].each_with_index do |path, i|
|
|
83
|
+
seed = Teek::Photo.new(app, width: 4, height: 4)
|
|
84
|
+
color = i.zero? ? [255, 0, 0, 255] : [0, 255, 0, 255]
|
|
85
|
+
seed.put_block((color.pack('CCCC')) * 16, 4, 4)
|
|
86
|
+
app.tcl_eval("#{seed.name} write {#{path}} -format png")
|
|
87
|
+
seed.delete
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
icon_a = nil
|
|
91
|
+
icon_b = nil
|
|
92
|
+
session = Teek::UI.app(title: 'Image Test') do |ui|
|
|
93
|
+
icon_a = ui.image(path_a)
|
|
94
|
+
icon_b = ui.image(path_b)
|
|
95
|
+
ui.label(:pic, image: icon_a)
|
|
96
|
+
end
|
|
97
|
+
session.run_async
|
|
98
|
+
session.app.update
|
|
99
|
+
|
|
100
|
+
pic_path = session[:pic].path
|
|
101
|
+
assert_equal icon_a.name, session.app.command(pic_path, :cget, '-image')
|
|
102
|
+
|
|
103
|
+
session[:pic].configure(image: icon_b)
|
|
104
|
+
|
|
105
|
+
assert_equal icon_b.name, session.app.command(pic_path, :cget, '-image')
|
|
106
|
+
|
|
107
|
+
session.app.destroy
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_declared_images_are_retained_on_the_session_and_not_leaked_out_from_under_the_widget
|
|
113
|
+
assert_tk_app("an image declared via ui.image should stay alive (retained on the session) for as long as the widget referencing it exists") do
|
|
114
|
+
require 'teek/ui'
|
|
115
|
+
require 'tmpdir'
|
|
116
|
+
|
|
117
|
+
Dir.mktmpdir do |dir|
|
|
118
|
+
path = File.join(dir, 'test.png')
|
|
119
|
+
seed = Teek::Photo.new(app, width: 4, height: 4)
|
|
120
|
+
seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
|
|
121
|
+
app.tcl_eval("#{seed.name} write {#{path}} -format png")
|
|
122
|
+
seed.delete
|
|
123
|
+
|
|
124
|
+
session = Teek::UI.app(title: 'Image Test') do |ui|
|
|
125
|
+
ui.label(:pic, image: ui.image(path))
|
|
126
|
+
end
|
|
127
|
+
session.run_async
|
|
128
|
+
session.app.update
|
|
129
|
+
|
|
130
|
+
# nothing outside the session holds a reference to the Image -
|
|
131
|
+
# only Session#images does. Forcing a GC pass here is the whole
|
|
132
|
+
# point: if the session weren't retaining it, this would be the
|
|
133
|
+
# moment the image gets collected (and the widget's -image would
|
|
134
|
+
# go stale/broken).
|
|
135
|
+
GC.start
|
|
136
|
+
|
|
137
|
+
assert_equal 1, session.images.length
|
|
138
|
+
image = session.images.first
|
|
139
|
+
assert_equal 'photo', session.app.tcl_eval("image type #{image.name}")
|
|
140
|
+
assert_equal image.name, session.app.command(session[:pic].path, :cget, '-image')
|
|
141
|
+
|
|
142
|
+
session.app.destroy
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|