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,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# ui.overlay floats a single widget on top of the enclosing ui.canvas at a
|
|
7
|
+
# fixed corner/edge/center anchor, via Tk's `place` geometry manager - the
|
|
8
|
+
# one legitimate "absolute position" case (a status readout or button bar
|
|
9
|
+
# layered over canvas content), not a general-purpose layout mode.
|
|
10
|
+
class TestOverlay < Minitest::Test
|
|
11
|
+
include TeekTestHelper
|
|
12
|
+
|
|
13
|
+
def test_overlay_places_the_widget_at_the_right_anchor
|
|
14
|
+
assert_tk_app("overlay should place its widget in the canvas at the anchor's relx/rely/anchor") do
|
|
15
|
+
require 'teek/ui'
|
|
16
|
+
|
|
17
|
+
session = Teek::UI.app(title: 'Overlay Test') do |ui|
|
|
18
|
+
ui.canvas(:board, width: 300, height: 200) do |cv|
|
|
19
|
+
cv.overlay(at: :bottom_right) { ui.label(:status, text: 'Ready') }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
session.run_async
|
|
23
|
+
session.app.update
|
|
24
|
+
|
|
25
|
+
info = session.app.tcl_eval("place info #{session[:status].path}")
|
|
26
|
+
assert_match(/-in #{Regexp.escape(session[:board].path)}(?:\s|$)/, info)
|
|
27
|
+
assert_match(/-relx 1(?:\s|$)/, info)
|
|
28
|
+
assert_match(/-rely 1(?:\s|$)/, info)
|
|
29
|
+
assert_match(/-anchor se(?:\s|$)/, info)
|
|
30
|
+
|
|
31
|
+
session.app.destroy
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_two_overlays_position_independently
|
|
36
|
+
assert_tk_app("two overlays on the same canvas should each land at their own anchor") do
|
|
37
|
+
require 'teek/ui'
|
|
38
|
+
|
|
39
|
+
session = Teek::UI.app(title: 'Overlay Test') do |ui|
|
|
40
|
+
ui.canvas(:board, width: 300, height: 200) do |cv|
|
|
41
|
+
cv.overlay(at: :top_left) { ui.label(:status, text: 'Ready') }
|
|
42
|
+
cv.overlay(at: :top_right) { ui.row(:controls) { ui.button(:pause, text: 'Pause') } }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
session.run_async
|
|
46
|
+
session.app.update
|
|
47
|
+
|
|
48
|
+
status_info = session.app.tcl_eval("place info #{session[:status].path}")
|
|
49
|
+
controls_info = session.app.tcl_eval("place info #{session[:controls].path}")
|
|
50
|
+
|
|
51
|
+
assert_match(/-relx 0(?:\s|$)/, status_info)
|
|
52
|
+
assert_match(/-rely 0(?:\s|$)/, status_info)
|
|
53
|
+
assert_match(/-anchor nw(?:\s|$)/, status_info)
|
|
54
|
+
|
|
55
|
+
assert_match(/-relx 1(?:\s|$)/, controls_info)
|
|
56
|
+
assert_match(/-rely 0(?:\s|$)/, controls_info)
|
|
57
|
+
assert_match(/-anchor ne(?:\s|$)/, controls_info)
|
|
58
|
+
|
|
59
|
+
session.app.destroy
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_overlay_tracks_a_canvas_resize
|
|
64
|
+
assert_tk_app("an overlay's real on-screen position should scale with the canvas, not stay fixed") do
|
|
65
|
+
require 'teek/ui'
|
|
66
|
+
|
|
67
|
+
session = Teek::UI.app(title: 'Overlay Test') do |ui|
|
|
68
|
+
ui.canvas(:board, width: 200, height: 200) do |cv|
|
|
69
|
+
cv.overlay(at: :bottom_right) { ui.label(:status, text: 'Ready') }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
session.run_async
|
|
73
|
+
session.app.update
|
|
74
|
+
|
|
75
|
+
before_x = session.app.tcl_eval("winfo x #{session[:status].path}").to_i
|
|
76
|
+
before_y = session.app.tcl_eval("winfo y #{session[:status].path}").to_i
|
|
77
|
+
|
|
78
|
+
session.app.tcl_eval("#{session[:board].path} configure -width 500 -height 500")
|
|
79
|
+
session.app.update
|
|
80
|
+
|
|
81
|
+
after_x = session.app.tcl_eval("winfo x #{session[:status].path}").to_i
|
|
82
|
+
after_y = session.app.tcl_eval("winfo y #{session[:status].path}").to_i
|
|
83
|
+
|
|
84
|
+
assert_operator after_x, :>, before_x, "the overlay should have followed the canvas's new bottom-right corner"
|
|
85
|
+
assert_operator after_y, :>, before_y, "the overlay should have followed the canvas's new bottom-right corner"
|
|
86
|
+
|
|
87
|
+
session.app.destroy
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_overlay_outside_a_canvas_raises
|
|
92
|
+
assert_tk_app("overlay declared outside ui.canvas should raise immediately, not silently do nothing") do
|
|
93
|
+
require 'teek/ui'
|
|
94
|
+
|
|
95
|
+
error = assert_raises(ArgumentError) do
|
|
96
|
+
Teek::UI.app(title: 'Overlay Test') { |ui| ui.overlay(at: :top_left) { ui.label(text: 'oops') } }
|
|
97
|
+
end
|
|
98
|
+
assert_match(/ui\.canvas/i, error.message)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_overlay_with_an_invalid_anchor_raises
|
|
103
|
+
assert_tk_app("overlay given an unrecognized at: should raise, not silently no-op") do
|
|
104
|
+
require 'teek/ui'
|
|
105
|
+
|
|
106
|
+
error = assert_raises(ArgumentError) do
|
|
107
|
+
Teek::UI.app(title: 'Overlay Test') do |ui|
|
|
108
|
+
ui.canvas(:board, width: 200, height: 200) { |cv| cv.overlay(at: :middle) { ui.label(text: 'oops') } }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
assert_match(/at:/, error.message)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_overlay_requiring_exactly_one_widget_raises_on_zero_or_many
|
|
116
|
+
assert_tk_app("overlay's block must build exactly one widget") do
|
|
117
|
+
require 'teek/ui'
|
|
118
|
+
|
|
119
|
+
empty_error = assert_raises(ArgumentError) do
|
|
120
|
+
Teek::UI.app(title: 'Overlay Test') do |ui|
|
|
121
|
+
ui.canvas(:board, width: 200, height: 200) { |cv| cv.overlay(at: :top_left) { } }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
assert_match(/exactly one/i, empty_error.message)
|
|
125
|
+
|
|
126
|
+
many_error = assert_raises(ArgumentError) do
|
|
127
|
+
Teek::UI.app(title: 'Overlay Test') do |ui|
|
|
128
|
+
ui.canvas(:board, width: 200, height: 200) do |cv|
|
|
129
|
+
cv.overlay(at: :top_left) do
|
|
130
|
+
ui.label(text: 'one')
|
|
131
|
+
ui.label(text: 'two')
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
assert_match(/exactly one/i, many_error.message)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/test/test_raw.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
class TestRaw < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_raw_runs_at_realize_with_a_working_live_app
|
|
10
|
+
assert_tk_app("ui.raw's block should run at realize, with a real, usable app") do
|
|
11
|
+
require 'teek/ui'
|
|
12
|
+
|
|
13
|
+
session = Teek::UI.app(title: 'Raw Test') do |ui|
|
|
14
|
+
ui.raw { |app| app.command('ttk::label', '.raw_label', text: 'from raw') }
|
|
15
|
+
end
|
|
16
|
+
session.run_async
|
|
17
|
+
session.app.update
|
|
18
|
+
|
|
19
|
+
assert session.app.winfo.exists?('.raw_label')
|
|
20
|
+
assert_equal 'from raw', session.app.command('.raw_label', :cget, '-text')
|
|
21
|
+
|
|
22
|
+
session.app.destroy
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_raw_does_not_run_before_realize
|
|
27
|
+
assert_tk_app("ui.raw's block should not fire during build, only at realize") do
|
|
28
|
+
require 'teek/ui'
|
|
29
|
+
|
|
30
|
+
executed = false
|
|
31
|
+
session = Teek::UI.app(title: 'Raw Test') { |ui| ui.raw { |_app| executed = true } }
|
|
32
|
+
|
|
33
|
+
refute executed, "raw block ran during build, before any interpreter existed"
|
|
34
|
+
|
|
35
|
+
session.realize
|
|
36
|
+
assert executed, "raw block did not run at realize"
|
|
37
|
+
|
|
38
|
+
session.app.destroy
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_raw_op_resolves_a_forward_referenced_sibling_by_name
|
|
43
|
+
assert_tk_app("a raw op declared before a sibling it references should still resolve its realized path") do
|
|
44
|
+
require 'teek/ui'
|
|
45
|
+
|
|
46
|
+
session = Teek::UI.app(title: 'Raw Test') do |ui|
|
|
47
|
+
ui.raw { |app| app.command(ui[:later].path, :configure, text: 'Changed by raw') }
|
|
48
|
+
ui.button(:later, text: 'Original')
|
|
49
|
+
end
|
|
50
|
+
session.run_async
|
|
51
|
+
session.app.update
|
|
52
|
+
|
|
53
|
+
assert_equal 'Changed by raw', session.app.command(session[:later].path, :cget, '-text')
|
|
54
|
+
|
|
55
|
+
session.app.destroy
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
class TestReactiveVars < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_slider_and_label_stay_in_sync_bidirectionally
|
|
10
|
+
assert_tk_app("a var bound to a slider and a label should keep both in sync") do
|
|
11
|
+
require 'teek/ui'
|
|
12
|
+
|
|
13
|
+
speed = nil
|
|
14
|
+
session = Teek::UI.app(title: 'Reactive Vars Test') do |ui|
|
|
15
|
+
speed = ui.var(5)
|
|
16
|
+
ui.slider(:speed_slider, from: 1, to: 10, bind: speed)
|
|
17
|
+
ui.label(:speed_label, bind: speed)
|
|
18
|
+
end
|
|
19
|
+
session.run_async
|
|
20
|
+
session.app.update
|
|
21
|
+
|
|
22
|
+
assert_equal 5, speed.value
|
|
23
|
+
|
|
24
|
+
slider_path = session[:speed_slider].path
|
|
25
|
+
label_path = session[:speed_label].path
|
|
26
|
+
|
|
27
|
+
# slider -> var -> label
|
|
28
|
+
# ttk::scale always stores/formats its bound variable as a float
|
|
29
|
+
# (e.g. "7.0"), even for a whole-number -from/-to range - compare
|
|
30
|
+
# numerically rather than assuming a particular string format.
|
|
31
|
+
session.app.command(slider_path, :set, 7)
|
|
32
|
+
session.app.update
|
|
33
|
+
assert_equal 7, speed.value
|
|
34
|
+
assert_equal 7.0, session.app.command(label_path, :cget, '-text').to_f
|
|
35
|
+
|
|
36
|
+
# var -> slider and label
|
|
37
|
+
speed.value = 3
|
|
38
|
+
session.app.update
|
|
39
|
+
assert_equal 3.0, session.app.command(slider_path, :get).to_f
|
|
40
|
+
assert_equal 3.0, session.app.command(label_path, :cget, '-text').to_f
|
|
41
|
+
|
|
42
|
+
session.app.destroy
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_text_box_is_two_way
|
|
47
|
+
assert_tk_app("a var bound to a text_box should sync in both directions") do
|
|
48
|
+
require 'teek/ui'
|
|
49
|
+
|
|
50
|
+
name = nil
|
|
51
|
+
session = Teek::UI.app(title: 'Reactive Vars Test') { |ui| name = ui.var(''); ui.text_box(:name_box, bind: name) }
|
|
52
|
+
session.run_async
|
|
53
|
+
session.app.update
|
|
54
|
+
|
|
55
|
+
# var -> text_box
|
|
56
|
+
name.value = 'hello'
|
|
57
|
+
session.app.update
|
|
58
|
+
assert_equal 'hello', session.app.command(session[:name_box].path, :get)
|
|
59
|
+
|
|
60
|
+
# text_box -> var
|
|
61
|
+
box_path = session[:name_box].path
|
|
62
|
+
session.app.command(box_path, :delete, 0, :end)
|
|
63
|
+
session.app.command(box_path, :insert, 0, 'typed')
|
|
64
|
+
session.app.update
|
|
65
|
+
assert_equal 'typed', name.value
|
|
66
|
+
|
|
67
|
+
session.app.destroy
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_on_change_fires_with_the_coerced_value_regardless_of_which_widget_changed_it
|
|
72
|
+
assert_tk_app("on_change should fire with a coerced Integer, triggered by the bound widget") do
|
|
73
|
+
require 'teek/ui'
|
|
74
|
+
|
|
75
|
+
speed = nil
|
|
76
|
+
changes = []
|
|
77
|
+
session = Teek::UI.app(title: 'Reactive Vars Test') do |ui|
|
|
78
|
+
speed = ui.var(5)
|
|
79
|
+
speed.on_change { |v| changes << v }
|
|
80
|
+
ui.slider(:speed_slider, from: 1, to: 10, bind: speed)
|
|
81
|
+
end
|
|
82
|
+
session.run_async
|
|
83
|
+
session.app.update
|
|
84
|
+
|
|
85
|
+
session.app.command(session[:speed_slider].path, :set, 9)
|
|
86
|
+
session.app.update
|
|
87
|
+
|
|
88
|
+
assert_includes changes, 9
|
|
89
|
+
assert_kind_of Integer, changes.last
|
|
90
|
+
|
|
91
|
+
session.app.destroy
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_boolean_var_coerces_correctly_for_a_checkbox
|
|
96
|
+
assert_tk_app("a Boolean var bound to a checkbox should use Tk's 1/0 convention and coerce back to true/false") do
|
|
97
|
+
require 'teek/ui'
|
|
98
|
+
|
|
99
|
+
enabled = nil
|
|
100
|
+
session = Teek::UI.app(title: 'Reactive Vars Test') do |ui|
|
|
101
|
+
enabled = ui.var(true)
|
|
102
|
+
ui.checkbox(:enabled_box, bind: enabled)
|
|
103
|
+
end
|
|
104
|
+
session.run_async
|
|
105
|
+
session.app.update
|
|
106
|
+
|
|
107
|
+
assert_equal true, enabled.value
|
|
108
|
+
assert_equal '1', session.app.get_variable(enabled.name)
|
|
109
|
+
|
|
110
|
+
enabled.value = false
|
|
111
|
+
session.app.update
|
|
112
|
+
assert_equal false, enabled.value
|
|
113
|
+
assert_equal '0', session.app.get_variable(enabled.name)
|
|
114
|
+
|
|
115
|
+
session.app.destroy
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
class TestRealizer < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
def test_realize_creates_real_mapped_widgets_with_hierarchical_paths
|
|
10
|
+
assert_tk_app("realizing a nested tree should create real, mapped widgets at hierarchical paths") do
|
|
11
|
+
require 'teek/ui'
|
|
12
|
+
|
|
13
|
+
session = Teek::UI.app(title: 'Realizer Test') do |ui|
|
|
14
|
+
ui.panel(:controls) do |p|
|
|
15
|
+
p.button(:go, text: 'Go')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
session.run_async
|
|
19
|
+
session.app.update
|
|
20
|
+
|
|
21
|
+
go_handle = session[:go]
|
|
22
|
+
assert_equal '.controls.go', go_handle.path
|
|
23
|
+
assert session.app.winfo.exists?(go_handle.path)
|
|
24
|
+
assert session.app.winfo.ismapped?(go_handle.path), "realized widgets should be packed/visible, not just created"
|
|
25
|
+
|
|
26
|
+
session.app.destroy
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_unnamed_nodes_get_a_valid_auto_generated_path_segment
|
|
31
|
+
assert_tk_app("an unnamed node should still realize to a real, addressable Tk path") do
|
|
32
|
+
require 'teek/ui'
|
|
33
|
+
|
|
34
|
+
handle = nil
|
|
35
|
+
session = Teek::UI.app(title: 'Realizer Test') { |ui| handle = ui.label(text: 'Hi') }
|
|
36
|
+
session.run_async
|
|
37
|
+
session.app.update
|
|
38
|
+
|
|
39
|
+
assert session.app.winfo.exists?(handle.path)
|
|
40
|
+
# unnamed nodes get their Document-assigned key (e.g. "#anon1") as
|
|
41
|
+
# their path segment - unique for the whole document, not a
|
|
42
|
+
# per-Realizer-instance counter, so it stays collision-free across
|
|
43
|
+
# separate realize_subtree calls too (see Session#add).
|
|
44
|
+
assert_match(/\A\.\S+\z/, handle.path)
|
|
45
|
+
|
|
46
|
+
session.app.destroy
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_handle_configure_mutates_the_live_widget_post_realize
|
|
51
|
+
assert_tk_app("handle.configure after realize should mutate the real widget") do
|
|
52
|
+
require 'teek/ui'
|
|
53
|
+
|
|
54
|
+
session = Teek::UI.app(title: 'Realizer Test') { |ui| ui.label(:greeting, text: 'Hi') }
|
|
55
|
+
session.run_async
|
|
56
|
+
session.app.update
|
|
57
|
+
|
|
58
|
+
session[:greeting].configure(text: 'Bye')
|
|
59
|
+
|
|
60
|
+
assert_equal 'Bye', session.app.command(session[:greeting].path, :cget, '-text')
|
|
61
|
+
|
|
62
|
+
session.app.destroy
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_forward_event_target_reference_resolves_and_fires
|
|
67
|
+
assert_tk_app("an event binding targeting a widget declared later should resolve once the whole tree is realized") do
|
|
68
|
+
require 'teek/ui'
|
|
69
|
+
|
|
70
|
+
fired = false
|
|
71
|
+
session = Teek::UI.app(title: 'Realizer Test') do |ui|
|
|
72
|
+
ui.button(:trigger, text: 'Trigger')
|
|
73
|
+
ui.label(:downstream, text: 'Target') # declared AFTER :trigger
|
|
74
|
+
|
|
75
|
+
# white-box: no event DSL yet (that's a separate bead), so attach the
|
|
76
|
+
# binding directly the way the future DSL will - a forward reference
|
|
77
|
+
# by name to a node the realizer hasn't created yet at build time.
|
|
78
|
+
ui.document.find(:trigger).events <<
|
|
79
|
+
Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { fired = true }, target: :downstream)
|
|
80
|
+
end
|
|
81
|
+
session.run_async
|
|
82
|
+
session.app.update
|
|
83
|
+
|
|
84
|
+
downstream_path = session[:downstream].path
|
|
85
|
+
session.app.tcl_eval("event generate #{downstream_path} <Button-1>")
|
|
86
|
+
session.app.update
|
|
87
|
+
|
|
88
|
+
assert fired, "the forward-referenced target's binding did not fire"
|
|
89
|
+
|
|
90
|
+
session.app.destroy
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_event_target_inside_a_component_resolves_to_that_components_own_node_not_a_siblings
|
|
95
|
+
assert_tk_app("a binding target inside a component should resolve to THAT component's like-named node, never a sibling component's") do
|
|
96
|
+
require 'teek/ui'
|
|
97
|
+
|
|
98
|
+
a_fired = false
|
|
99
|
+
b_fired = false
|
|
100
|
+
comp_a_downstream = nil
|
|
101
|
+
session = Teek::UI.app(title: 'Realizer Test') do |ui|
|
|
102
|
+
# each component nested under its own panel, matching how
|
|
103
|
+
# test_component_realtk.rb mounts like-named siblings - two
|
|
104
|
+
# components' children only get distinct real Tk paths when they
|
|
105
|
+
# sit under different real parents.
|
|
106
|
+
ui.panel(:panel_a) do |p|
|
|
107
|
+
p.component { |c| c.button(:trigger, text: 'Trigger A'); comp_a_downstream = c.label(:downstream, text: 'Target A') }
|
|
108
|
+
end
|
|
109
|
+
ui.panel(:panel_b) do |p|
|
|
110
|
+
p.component { |c| c.button(:trigger, text: 'Trigger B'); c.label(:downstream, text: 'Target B') }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# white-box: target: isn't public DSL yet (see the plain forward-
|
|
114
|
+
# reference test above) - a component's own nodes aren't reachable
|
|
115
|
+
# by name from outside its scope, so walk the tree structurally
|
|
116
|
+
# (each panel has one component's 2 children as its own children,
|
|
117
|
+
# in build order) to attach each component's own binding. A
|
|
118
|
+
# binding's event listener attaches to the TARGET node's path (see
|
|
119
|
+
# the plain forward-reference test above), so generating the event
|
|
120
|
+
# on component A's own :downstream widget below only fires
|
|
121
|
+
# component A's binding if resolution correctly stayed within
|
|
122
|
+
# component A's scope.
|
|
123
|
+
comp_a_trigger_node, comp_a_downstream_node = ui.document.find(:panel_a).children
|
|
124
|
+
comp_b_trigger_node, comp_b_downstream_node = ui.document.find(:panel_b).children
|
|
125
|
+
comp_a_trigger_node.events <<
|
|
126
|
+
Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { a_fired = true }, target: :downstream)
|
|
127
|
+
comp_b_trigger_node.events <<
|
|
128
|
+
Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { b_fired = true }, target: :downstream)
|
|
129
|
+
raise "unexpected build shape" unless comp_a_downstream_node && comp_b_downstream_node
|
|
130
|
+
end
|
|
131
|
+
session.run_async
|
|
132
|
+
session.app.update
|
|
133
|
+
|
|
134
|
+
session.app.tcl_eval("event generate #{comp_a_downstream.path} <Button-1>")
|
|
135
|
+
session.app.update
|
|
136
|
+
|
|
137
|
+
assert a_fired, "component A's own trigger should have fired component A's own binding"
|
|
138
|
+
refute b_fired, "component A's trigger firing should never fire component B's binding"
|
|
139
|
+
|
|
140
|
+
session.app.destroy
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_event_target_forward_reference_still_resolves_inside_a_component
|
|
145
|
+
assert_tk_app("a forward reference to a not-yet-built sibling should still resolve when both are inside the same component") do
|
|
146
|
+
require 'teek/ui'
|
|
147
|
+
|
|
148
|
+
fired = false
|
|
149
|
+
session = Teek::UI.app(title: 'Realizer Test') do |ui|
|
|
150
|
+
ui.component do |c|
|
|
151
|
+
c.button(:trigger, text: 'Trigger')
|
|
152
|
+
c.label(:downstream, text: 'Target') # declared AFTER :trigger, same component
|
|
153
|
+
|
|
154
|
+
trigger_node, = c.document.root.children
|
|
155
|
+
trigger_node.events <<
|
|
156
|
+
Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { fired = true }, target: :downstream)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
session.run_async
|
|
160
|
+
session.app.update
|
|
161
|
+
|
|
162
|
+
_trigger_node, downstream_node = session.document.root.children
|
|
163
|
+
session.app.tcl_eval("event generate #{downstream_node.realized.path} <Button-1>")
|
|
164
|
+
session.app.update
|
|
165
|
+
|
|
166
|
+
assert fired, "the intra-component forward-referenced target's binding did not fire"
|
|
167
|
+
|
|
168
|
+
session.app.destroy
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_realize_is_atomic_on_a_mid_realize_error
|
|
173
|
+
assert_tk_app("an error partway through realize should leave the root window unmapped and the session not realized") do
|
|
174
|
+
require 'teek/ui'
|
|
175
|
+
|
|
176
|
+
session = Teek::UI.app(title: 'Realizer Test') do |ui|
|
|
177
|
+
ui.label(:first, text: 'Ok')
|
|
178
|
+
ui.document.root.add_child(Teek::UI::Node.new(type: :not_a_real_widget_type, name: :bad))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
assert_raises(StandardError) { session.realize }
|
|
182
|
+
|
|
183
|
+
assert_raises(Teek::UI::NotRealizedError) { session.app }
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Locks the invariant the validator's "mixed pack+grid" note relies on
|
|
188
|
+
# instead of checking directly: every container realizes into its own
|
|
189
|
+
# dedicated Tk master, and arrange_children picks exactly one geometry
|
|
190
|
+
# manager per master, so no master should ever end up with a mix of
|
|
191
|
+
# pack- and grid-managed children. Verified against the REAL, realized
|
|
192
|
+
# Tk widget tree (winfo children/winfo manager), not just teek-ui's own
|
|
193
|
+
# Document structure.
|
|
194
|
+
def test_no_realized_master_ever_receives_more_than_one_geometry_manager
|
|
195
|
+
assert_tk_app("every realized master frame should be managed by exactly one geometry manager - representative nested tree: column > grid > row > panel") do
|
|
196
|
+
require 'teek/ui'
|
|
197
|
+
|
|
198
|
+
session = Teek::UI.app(title: 'Realizer Test') do |ui|
|
|
199
|
+
ui.column(:outer, gap: 4) do |c|
|
|
200
|
+
c.grid(:g, gap: 2) do |g|
|
|
201
|
+
g.cell(row: 0, col: 0) { g.label(:l1, text: 'A') }
|
|
202
|
+
g.cell(row: 0, col: 1) do
|
|
203
|
+
g.row(:r, gap: 2) do |r|
|
|
204
|
+
r.button(:b1, text: 'B')
|
|
205
|
+
r.panel(:p) { |p| p.button(:b2, text: 'C') }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
c.button(:outer_btn, text: 'D')
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
session.run_async
|
|
213
|
+
session.app.update
|
|
214
|
+
|
|
215
|
+
# walks the REAL, realized Tk widget tree (winfo children/winfo
|
|
216
|
+
# manager) rather than teek-ui's own Document structure - this is
|
|
217
|
+
# what would actually misbehave if the invariant ever broke, wherever
|
|
218
|
+
# the break came from. A local recursive lambda, not a helper method -
|
|
219
|
+
# assert_tk_app re-evaluates this block's own source text in a
|
|
220
|
+
# separate worker context, which has no access to methods defined on
|
|
221
|
+
# the surrounding Minitest::Test class.
|
|
222
|
+
check = lambda do |path|
|
|
223
|
+
children = session.app.split_list(session.app.tcl_eval("winfo children #{path}"))
|
|
224
|
+
managers = children.map { |child| session.app.tcl_eval("winfo manager #{child}") }.reject(&:empty?).uniq
|
|
225
|
+
assert_operator managers.length, :<=, 1,
|
|
226
|
+
"#{path} has children managed by more than one geometry manager: #{managers.inspect}"
|
|
227
|
+
children.each { |child| check.call(child) }
|
|
228
|
+
end
|
|
229
|
+
check.call('.')
|
|
230
|
+
|
|
231
|
+
session.app.destroy
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# A genuinely mixed master turns out to be impossible to even construct
|
|
236
|
+
# via pack/grid themselves - Tk's own geometry-manager framework refuses
|
|
237
|
+
# a second manager type for a master once one is active, symmetrically
|
|
238
|
+
# (pack-then-grid and grid-then-pack both raise), with a clear, immediate
|
|
239
|
+
# Teek::TclError - NOT a silent hang, contrary to the original "Tk
|
|
240
|
+
# literally HANGS on this" framing this project started from. Confirmed
|
|
241
|
+
# empirically - place coexists safely with either, confirming overlay
|
|
242
|
+
# (not built yet, place-based) is a genuinely different, safe case. This
|
|
243
|
+
# is a second, independent safety net on top of the realizer's own
|
|
244
|
+
# one-manager-per-master construction invariant the test above locks -
|
|
245
|
+
# not a replacement for it.
|
|
246
|
+
def test_tk_itself_refuses_a_second_geometry_manager_on_one_master
|
|
247
|
+
assert_tk_app("Tk's own geometry-manager protection should refuse pack+grid mixing on one master with a clear error") do
|
|
248
|
+
require 'teek/ui'
|
|
249
|
+
|
|
250
|
+
session = Teek::UI.app(title: 'Realizer Test') { |ui| ui.panel(:guarded) }
|
|
251
|
+
session.run_async
|
|
252
|
+
session.app.update
|
|
253
|
+
|
|
254
|
+
session.app.command('ttk::button', '.guarded.a', text: 'A')
|
|
255
|
+
session.app.command('ttk::button', '.guarded.b', text: 'B')
|
|
256
|
+
session.app.command(:pack, '.guarded.a')
|
|
257
|
+
|
|
258
|
+
error = assert_raises(Teek::TclError) { session.app.command(:grid, '.guarded.b', row: 0, column: 0) }
|
|
259
|
+
assert_match(/geometry manager/i, error.message)
|
|
260
|
+
|
|
261
|
+
session.app.destroy
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
data/test/test_scope.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'teek/ui/scope'
|
|
5
|
+
|
|
6
|
+
class TestScope < Minitest::Test
|
|
7
|
+
def test_top_level_is_top_level
|
|
8
|
+
assert Teek::UI::Scope::TOP_LEVEL.top_level?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_a_fresh_scope_is_not_top_level
|
|
12
|
+
refute Teek::UI::Scope.new.top_level?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_two_scopes_with_the_same_label_are_never_the_same_scope
|
|
16
|
+
a = Teek::UI::Scope.new(:widget)
|
|
17
|
+
b = Teek::UI::Scope.new(:widget)
|
|
18
|
+
|
|
19
|
+
refute a.equal?(b)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_parent_defaults_to_nil
|
|
23
|
+
assert_nil Teek::UI::Scope.new.parent
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_parent_is_settable
|
|
27
|
+
parent = Teek::UI::Scope.new
|
|
28
|
+
child = Teek::UI::Scope.new(:child, parent: parent)
|
|
29
|
+
|
|
30
|
+
assert_same parent, child.parent
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_label_is_readable
|
|
34
|
+
scope = Teek::UI::Scope.new(:sidebar)
|
|
35
|
+
|
|
36
|
+
assert_equal :sidebar, scope.label
|
|
37
|
+
end
|
|
38
|
+
end
|