ratatui_ruby 0.5.0 → 0.6.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 +4 -4
- data/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +6 -0
- data/CHANGELOG.md +44 -7
- data/README.md +11 -4
- data/REUSE.toml +2 -7
- data/doc/application_architecture.md +84 -10
- data/doc/application_testing.md +75 -29
- data/doc/contributors/design/ruby_frontend.md +39 -3
- data/doc/contributors/design/rust_backend.md +1 -0
- data/doc/contributors/developing_examples.md +129 -44
- data/doc/contributors/examples_audit/p1_high.md +21 -0
- data/doc/contributors/examples_audit/p2_moderate.md +81 -0
- data/doc/contributors/examples_audit.md +41 -0
- data/doc/event_handling.md +11 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart_demo.png +0 -0
- data/doc/images/widget_block_demo.png +0 -0
- data/doc/images/widget_canvas_demo.png +0 -0
- data/doc/images/widget_cell_demo.png +0 -0
- data/doc/images/widget_center_demo.png +0 -0
- data/doc/images/widget_chart_demo.png +0 -0
- data/doc/images/widget_list_demo.png +0 -0
- data/doc/images/widget_overlay_demo.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_sparkline_demo.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/quickstart.md +69 -76
- data/doc/terminal_limitations.md +92 -0
- data/examples/app_all_events/README.md +45 -27
- data/examples/app_all_events/app.rb +38 -35
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_entry.rb +17 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +8 -8
- data/examples/app_all_events/view/controls_view.rb +8 -6
- data/examples/app_all_events/view/counts_view.rb +12 -8
- data/examples/app_all_events/view/live_view.rb +8 -7
- data/examples/app_all_events/view/log_view.rb +10 -15
- data/examples/app_color_picker/README.md +84 -44
- data/examples/app_color_picker/app.rb +24 -62
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +45 -49
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/input.rb +99 -67
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +55 -26
- data/examples/app_login_form/README.md +47 -0
- data/examples/app_login_form/app.rb +2 -3
- data/examples/app_stateful_interaction/README.md +31 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +48 -0
- data/examples/verify_quickstart_dsl/app.rb +2 -0
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +2 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +8 -2
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +8 -2
- data/examples/widget_barchart_demo/README.md +49 -0
- data/examples/widget_barchart_demo/app.rb +5 -5
- data/examples/widget_block_demo/README.md +34 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_canvas_demo/README.md +27 -0
- data/examples/widget_canvas_demo/app.rb +123 -0
- data/examples/widget_cell_demo/README.md +36 -0
- data/examples/widget_cell_demo/app.rb +31 -24
- data/examples/widget_center_demo/README.md +29 -0
- data/examples/widget_center_demo/app.rb +116 -0
- data/examples/widget_chart_demo/README.md +41 -0
- data/examples/widget_chart_demo/app.rb +7 -2
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +91 -107
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{app_map_demo → widget_map_demo}/app.rb +2 -2
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +62 -33
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +0 -1
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +4 -3
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/{app_table_select → widget_table_demo}/app.rb +46 -8
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +15 -1
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +106 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +238 -26
- data/ext/ratatui_ruby/src/frame.rs +113 -1
- data/ext/ratatui_ruby/src/lib.rs +34 -4
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/terminal.rs +39 -15
- data/ext/ratatui_ruby/src/text.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
- data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +113 -1
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/lib/ratatui_ruby/cell.rb +4 -4
- data/lib/ratatui_ruby/event/key/character.rb +35 -0
- data/lib/ratatui_ruby/event/key/media.rb +44 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
- data/lib/ratatui_ruby/event/key/system.rb +45 -0
- data/lib/ratatui_ruby/event/key.rb +111 -51
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/frame.rb +96 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/cursor.rb +5 -0
- data/lib/ratatui_ruby/schema/gauge.rb +3 -1
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +25 -4
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/rect.rb +43 -0
- data/lib/ratatui_ruby/schema/style.rb +24 -4
- data/lib/ratatui_ruby/schema/table.rb +21 -3
- data/lib/ratatui_ruby/schema/text.rb +69 -1
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/session/autodoc.rb +65 -0
- data/lib/ratatui_ruby/session.rb +22 -7
- data/lib/ratatui_ruby/table_state.rb +90 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
- data/lib/ratatui_ruby/test_helper.rb +65 -358
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +42 -19
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
- data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
- data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +2 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/list.rbs +4 -2
- data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
- data/sig/ratatui_ruby/schema/rect.rbs +3 -0
- data/sig/ratatui_ruby/schema/style.rbs +3 -3
- data/sig/ratatui_ruby/schema/table.rbs +3 -1
- data/sig/ratatui_ruby/schema/text.rbs +8 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +13 -0
- data/sig/ratatui_ruby/table_state.rbs +15 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
- data/sig/ratatui_ruby/test_helper.rbs +5 -4
- data/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc/inventory.rb +9 -7
- data/tasks/autodoc.rake +11 -5
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/links.rb +67 -0
- data/tasks/sourcehut.rake +61 -21
- data/tasks/terminal_preview/app_screenshot.rb +13 -3
- data/tasks/terminal_preview/saved_screenshot.rb +4 -3
- metadata +111 -37
- data/doc/images/app_table_select.png +0 -0
- data/doc/images/widget_block_padding.png +0 -0
- data/doc/images/widget_block_titles.png +0 -0
- data/doc/images/widget_list_styles.png +0 -0
- data/examples/app_all_events/model/events.rb +0 -180
- data/examples/app_all_events/model/highlight.rb +0 -57
- data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
- data/examples/app_all_events/view_state.rb +0 -42
- data/examples/app_color_picker/scene.rb +0 -201
- data/examples/widget_block_padding/app.rb +0 -67
- data/examples/widget_block_titles/app.rb +0 -69
- data/examples/widget_list_styles/app.rb +0 -141
- data/examples/widget_table_flex/app.rb +0 -95
- data/sig/examples/widget_block_padding/app.rbs +0 -11
- data/sig/examples/widget_block_titles/app.rbs +0 -11
- data/sig/examples/widget_list_styles/app.rbs +0 -11
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
module TestHelper
|
|
8
|
+
##
|
|
9
|
+
# Assertions for verifying cell-level styling in terminal UIs.
|
|
10
|
+
#
|
|
11
|
+
# TUI styling is invisible to plain text comparisons. Colors, bold, italic,
|
|
12
|
+
# and other modifiers define the visual hierarchy. Without style assertions,
|
|
13
|
+
# you cannot verify that your highlight is actually highlighted.
|
|
14
|
+
#
|
|
15
|
+
# This mixin provides assertions to check foreground, background, and modifiers
|
|
16
|
+
# at specific coordinates or across entire regions.
|
|
17
|
+
#
|
|
18
|
+
# Use it to verify selection highlights, error colors, or themed areas.
|
|
19
|
+
#
|
|
20
|
+
# === Examples
|
|
21
|
+
#
|
|
22
|
+
# # Single cell
|
|
23
|
+
# assert_cell_style(0, 0, fg: :red, modifiers: [:bold])
|
|
24
|
+
#
|
|
25
|
+
# # Foreground color at coordinate
|
|
26
|
+
# assert_color(:green, x: 5, y: 2)
|
|
27
|
+
#
|
|
28
|
+
# # Entire header region
|
|
29
|
+
# assert_area_style({ x: 0, y: 0, w: 80, h: 1 }, bg: :blue)
|
|
30
|
+
#
|
|
31
|
+
module StyleAssertions
|
|
32
|
+
##
|
|
33
|
+
# Asserts that a cell has the expected style attributes.
|
|
34
|
+
#
|
|
35
|
+
# === Example
|
|
36
|
+
#
|
|
37
|
+
# assert_cell_style(0, 0, char: "H", fg: :red)
|
|
38
|
+
#
|
|
39
|
+
# [x] Integer x-coordinate.
|
|
40
|
+
# [y] Integer y-coordinate.
|
|
41
|
+
# [expected_attributes] Hash of attribute names to expected values.
|
|
42
|
+
def assert_cell_style(x, y, **expected_attributes)
|
|
43
|
+
cell = get_cell(x, y)
|
|
44
|
+
expected_attributes.each do |key, value|
|
|
45
|
+
actual_value = cell.public_send(key)
|
|
46
|
+
if value.nil?
|
|
47
|
+
assert_nil actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=nil, but got #{actual_value.inspect}"
|
|
48
|
+
else
|
|
49
|
+
assert_equal value, actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=#{value.inspect}, but got #{actual_value.inspect}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Asserts foreground or background color at a coordinate.
|
|
56
|
+
#
|
|
57
|
+
# Accepts symbols (<tt>:red</tt>), indexed colors (integers), or hex strings.
|
|
58
|
+
#
|
|
59
|
+
# === Examples
|
|
60
|
+
#
|
|
61
|
+
# assert_color(:red, x: 10, y: 5)
|
|
62
|
+
# assert_color(5, x: 10, y: 5, layer: :bg)
|
|
63
|
+
# assert_color("#ff00ff", x: 10, y: 5)
|
|
64
|
+
#
|
|
65
|
+
# [expected] Symbol, Integer, or String (hex).
|
|
66
|
+
# [x] Integer x-coordinate.
|
|
67
|
+
# [y] Integer y-coordinate.
|
|
68
|
+
# [layer] <tt>:fg</tt> (default) or <tt>:bg</tt>.
|
|
69
|
+
def assert_color(expected, x:, y:, layer: :fg)
|
|
70
|
+
cell = get_cell(x, y)
|
|
71
|
+
actual = cell.public_send(layer)
|
|
72
|
+
|
|
73
|
+
# Normalize expected integer to symbol if needed (RatatuiRuby returns :indexed_N)
|
|
74
|
+
expected_normalized = if expected.is_a?(Integer)
|
|
75
|
+
:"indexed_#{expected}"
|
|
76
|
+
else
|
|
77
|
+
expected
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
assert_equal expected_normalized, actual,
|
|
81
|
+
"Expected #{layer} at (#{x}, #{y}) to be #{expected.inspect}, but got #{actual.inspect}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Asserts that all cells in an area have the expected style.
|
|
86
|
+
#
|
|
87
|
+
# === Examples
|
|
88
|
+
#
|
|
89
|
+
# header = RatatuiRuby::Rect.new(x: 0, y: 0, width: 80, height: 1)
|
|
90
|
+
# assert_area_style(header, bg: :blue, modifiers: [:bold])
|
|
91
|
+
#
|
|
92
|
+
# assert_area_style({ x: 0, y: 0, w: 10, h: 1 }, fg: :red)
|
|
93
|
+
#
|
|
94
|
+
# [area] Rect-like object or Hash with x, y, width/w, height/h.
|
|
95
|
+
# [attributes] Style attributes to verify.
|
|
96
|
+
def assert_area_style(area, **attributes)
|
|
97
|
+
if area.is_a?(Hash)
|
|
98
|
+
x = area[:x] || 0
|
|
99
|
+
y = area[:y] || 0
|
|
100
|
+
w = area[:width] || area[:w] || 0
|
|
101
|
+
h = area[:height] || area[:h] || 0
|
|
102
|
+
else
|
|
103
|
+
x = area.x
|
|
104
|
+
y = area.y
|
|
105
|
+
w = area.width
|
|
106
|
+
h = area.height
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
(y...(y + h)).each do |row|
|
|
110
|
+
(x...(x + w)).each do |col|
|
|
111
|
+
assert_cell_style(col, row, **attributes)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Asserts the foreground color at a coordinate.
|
|
118
|
+
#
|
|
119
|
+
# Convenience alias for <tt>assert_color(expected, x:, y:, layer: :fg)</tt>.
|
|
120
|
+
#
|
|
121
|
+
# === Example
|
|
122
|
+
#
|
|
123
|
+
# assert_fg_color(:yellow, 0, 2)
|
|
124
|
+
#
|
|
125
|
+
# [expected] Symbol, Integer, or String (hex).
|
|
126
|
+
# [x] Integer x-coordinate.
|
|
127
|
+
# [y] Integer y-coordinate.
|
|
128
|
+
def assert_fg_color(expected, x, y)
|
|
129
|
+
assert_color(expected, x:, y:, layer: :fg)
|
|
130
|
+
end
|
|
131
|
+
alias assert_fg assert_fg_color
|
|
132
|
+
|
|
133
|
+
##
|
|
134
|
+
# Asserts the background color at a coordinate.
|
|
135
|
+
#
|
|
136
|
+
# Convenience alias for <tt>assert_color(expected, x:, y:, layer: :bg)</tt>.
|
|
137
|
+
#
|
|
138
|
+
# === Example
|
|
139
|
+
#
|
|
140
|
+
# assert_bg_color(:blue, 0, 2)
|
|
141
|
+
#
|
|
142
|
+
# [expected] Symbol, Integer, or String (hex).
|
|
143
|
+
# [x] Integer x-coordinate.
|
|
144
|
+
# [y] Integer y-coordinate.
|
|
145
|
+
def assert_bg_color(expected, x, y)
|
|
146
|
+
assert_color(expected, x:, y:, layer: :bg)
|
|
147
|
+
end
|
|
148
|
+
alias assert_bg assert_bg_color
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# Asserts that a cell has the bold modifier.
|
|
152
|
+
#
|
|
153
|
+
# === Example
|
|
154
|
+
#
|
|
155
|
+
# assert_bold(0, 2)
|
|
156
|
+
#
|
|
157
|
+
# [x] Integer x-coordinate.
|
|
158
|
+
# [y] Integer y-coordinate.
|
|
159
|
+
def assert_bold(x, y)
|
|
160
|
+
cell = get_cell(x, y)
|
|
161
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
162
|
+
assert modifiers.include?(:bold),
|
|
163
|
+
"Expected cell at (#{x}, #{y}) to be bold, but modifiers were #{modifiers.inspect}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
##
|
|
167
|
+
# Asserts that a cell has the italic modifier.
|
|
168
|
+
#
|
|
169
|
+
# === Example
|
|
170
|
+
#
|
|
171
|
+
# assert_italic(0, 2)
|
|
172
|
+
#
|
|
173
|
+
# [x] Integer x-coordinate.
|
|
174
|
+
# [y] Integer y-coordinate.
|
|
175
|
+
def assert_italic(x, y)
|
|
176
|
+
cell = get_cell(x, y)
|
|
177
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
178
|
+
assert modifiers.include?(:italic),
|
|
179
|
+
"Expected cell at (#{x}, #{y}) to be italic, but modifiers were #{modifiers.inspect}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
##
|
|
183
|
+
# Asserts that a cell has the underlined modifier.
|
|
184
|
+
#
|
|
185
|
+
# === Example
|
|
186
|
+
#
|
|
187
|
+
# assert_underlined(0, 2)
|
|
188
|
+
#
|
|
189
|
+
# [x] Integer x-coordinate.
|
|
190
|
+
# [y] Integer y-coordinate.
|
|
191
|
+
def assert_underlined(x, y)
|
|
192
|
+
cell = get_cell(x, y)
|
|
193
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
194
|
+
assert modifiers.include?(:underlined),
|
|
195
|
+
"Expected cell at (#{x}, #{y}) to be underlined, but modifiers were #{modifiers.inspect}"
|
|
196
|
+
end
|
|
197
|
+
alias assert_underline assert_underlined
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# Asserts that a cell has the dim modifier.
|
|
201
|
+
#
|
|
202
|
+
# === Example
|
|
203
|
+
#
|
|
204
|
+
# assert_dim(0, 2)
|
|
205
|
+
#
|
|
206
|
+
# [x] Integer x-coordinate.
|
|
207
|
+
# [y] Integer y-coordinate.
|
|
208
|
+
def assert_dim(x, y)
|
|
209
|
+
cell = get_cell(x, y)
|
|
210
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
211
|
+
assert modifiers.include?(:dim),
|
|
212
|
+
"Expected cell at (#{x}, #{y}) to be dim, but modifiers were #{modifiers.inspect}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
##
|
|
216
|
+
# Asserts that a cell has the reversed (inverse video) modifier.
|
|
217
|
+
#
|
|
218
|
+
# === Example
|
|
219
|
+
#
|
|
220
|
+
# assert_reversed(0, 2)
|
|
221
|
+
#
|
|
222
|
+
# [x] Integer x-coordinate.
|
|
223
|
+
# [y] Integer y-coordinate.
|
|
224
|
+
def assert_reversed(x, y)
|
|
225
|
+
cell = get_cell(x, y)
|
|
226
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
227
|
+
assert modifiers.include?(:reversed),
|
|
228
|
+
"Expected cell at (#{x}, #{y}) to be reversed, but modifiers were #{modifiers.inspect}"
|
|
229
|
+
end
|
|
230
|
+
alias assert_inverse assert_reversed
|
|
231
|
+
alias assert_inverse_video assert_reversed
|
|
232
|
+
|
|
233
|
+
##
|
|
234
|
+
# Asserts that a cell has the crossed_out (strikethrough) modifier.
|
|
235
|
+
#
|
|
236
|
+
# === Example
|
|
237
|
+
#
|
|
238
|
+
# assert_crossed_out(0, 2)
|
|
239
|
+
#
|
|
240
|
+
# [x] Integer x-coordinate.
|
|
241
|
+
# [y] Integer y-coordinate.
|
|
242
|
+
def assert_crossed_out(x, y)
|
|
243
|
+
cell = get_cell(x, y)
|
|
244
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
245
|
+
assert modifiers.include?(:crossed_out),
|
|
246
|
+
"Expected cell at (#{x}, #{y}) to be crossed_out, but modifiers were #{modifiers.inspect}"
|
|
247
|
+
end
|
|
248
|
+
alias assert_strikethrough assert_crossed_out
|
|
249
|
+
alias assert_strike assert_crossed_out
|
|
250
|
+
|
|
251
|
+
##
|
|
252
|
+
# Asserts that a cell has the hidden modifier.
|
|
253
|
+
#
|
|
254
|
+
# [x] Integer x-coordinate.
|
|
255
|
+
# [y] Integer y-coordinate.
|
|
256
|
+
def assert_hidden(x, y)
|
|
257
|
+
cell = get_cell(x, y)
|
|
258
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
259
|
+
assert modifiers.include?(:hidden),
|
|
260
|
+
"Expected cell at (#{x}, #{y}) to be hidden, but modifiers were #{modifiers.inspect}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
##
|
|
264
|
+
# Asserts that a cell has the slow_blink modifier.
|
|
265
|
+
#
|
|
266
|
+
# [x] Integer x-coordinate.
|
|
267
|
+
# [y] Integer y-coordinate.
|
|
268
|
+
def assert_slow_blink(x, y)
|
|
269
|
+
cell = get_cell(x, y)
|
|
270
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
271
|
+
assert modifiers.include?(:slow_blink),
|
|
272
|
+
"Expected cell at (#{x}, #{y}) to have slow_blink, but modifiers were #{modifiers.inspect}"
|
|
273
|
+
end
|
|
274
|
+
alias assert_blink assert_slow_blink
|
|
275
|
+
|
|
276
|
+
##
|
|
277
|
+
# Asserts that a cell has the rapid_blink modifier.
|
|
278
|
+
#
|
|
279
|
+
# [x] Integer x-coordinate.
|
|
280
|
+
# [y] Integer y-coordinate.
|
|
281
|
+
def assert_rapid_blink(x, y)
|
|
282
|
+
cell = get_cell(x, y)
|
|
283
|
+
modifiers = (cell.modifiers || []).map(&:to_sym)
|
|
284
|
+
assert modifiers.include?(:rapid_blink),
|
|
285
|
+
"Expected cell at (#{x}, #{y}) to have rapid_blink, but modifiers were #{modifiers.inspect}"
|
|
286
|
+
end
|
|
287
|
+
# Color-specific assertion helpers.
|
|
288
|
+
#
|
|
289
|
+
# Manually specifying <tt>:red</tt> or <tt>:blue</tt> in every <tt>assert_color</tt> call is repetitive.
|
|
290
|
+
# It hides the intent of the test behind boilerplate arguments.
|
|
291
|
+
#
|
|
292
|
+
# These meta-programmed helpers provide a punchy, intent-focused API. Use them to
|
|
293
|
+
# verify colors with minimal ceremony.
|
|
294
|
+
#
|
|
295
|
+
# === Standard Foreground Color Aliases
|
|
296
|
+
#
|
|
297
|
+
# - <tt>assert_black(x, y)</tt>
|
|
298
|
+
# - <tt>assert_red(x, y)</tt>
|
|
299
|
+
# - <tt>assert_green(x, y)</tt>
|
|
300
|
+
# - <tt>assert_yellow(x, y)</tt>
|
|
301
|
+
# - <tt>assert_blue(x, y)</tt>
|
|
302
|
+
# - <tt>assert_magenta(x, y)</tt>
|
|
303
|
+
# - <tt>assert_cyan(x, y)</tt>
|
|
304
|
+
# - <tt>assert_gray(x, y)</tt>
|
|
305
|
+
# - <tt>assert_dark_gray(x, y)</tt>
|
|
306
|
+
# - <tt>assert_light_red(x, y)</tt>
|
|
307
|
+
# - <tt>assert_light_green(x, y)</tt>
|
|
308
|
+
# - <tt>assert_light_yellow(x, y)</tt>
|
|
309
|
+
# - <tt>assert_light_blue(x, y)</tt>
|
|
310
|
+
# - <tt>assert_light_magenta(x, y)</tt>
|
|
311
|
+
# - <tt>assert_light_cyan(x, y)</tt>
|
|
312
|
+
# - <tt>assert_white(x, y)</tt>
|
|
313
|
+
#
|
|
314
|
+
# === Standard Background Color Aliases
|
|
315
|
+
#
|
|
316
|
+
# - <tt>assert_bg_black(x, y)</tt>
|
|
317
|
+
# - <tt>assert_bg_red(x, y)</tt>
|
|
318
|
+
# - ...and so on for all standard colors.
|
|
319
|
+
[
|
|
320
|
+
:black,
|
|
321
|
+
:red,
|
|
322
|
+
:green,
|
|
323
|
+
:yellow,
|
|
324
|
+
:blue,
|
|
325
|
+
:magenta,
|
|
326
|
+
:cyan,
|
|
327
|
+
:gray,
|
|
328
|
+
:dark_gray,
|
|
329
|
+
:light_red,
|
|
330
|
+
:light_green,
|
|
331
|
+
:light_yellow,
|
|
332
|
+
:light_blue,
|
|
333
|
+
:light_magenta,
|
|
334
|
+
:light_cyan,
|
|
335
|
+
:white,
|
|
336
|
+
].each do |color|
|
|
337
|
+
# :method: assert_#{color}
|
|
338
|
+
# Asserts the foreground color at (x, y) is <tt>:#{color}</tt>.
|
|
339
|
+
define_method(:"assert_#{color}") do |x, y|
|
|
340
|
+
assert_fg_color(color, x, y)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# :method: assert_bg_#{color}
|
|
344
|
+
# Asserts the background color at (x, y) is <tt>:#{color}</tt>.
|
|
345
|
+
define_method(:"assert_bg_#{color}") do |x, y|
|
|
346
|
+
assert_bg_color(color, x, y)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "timeout"
|
|
7
|
+
require "minitest/mock"
|
|
8
|
+
|
|
9
|
+
module RatatuiRuby
|
|
10
|
+
module TestHelper
|
|
11
|
+
##
|
|
12
|
+
# Terminal setup and buffer inspection for TUI tests.
|
|
13
|
+
#
|
|
14
|
+
# Testing TUIs against a real terminal is slow, flaky, and hard to automate.
|
|
15
|
+
# Initializing, cleaning up, and inspecting terminal state by hand is tedious.
|
|
16
|
+
#
|
|
17
|
+
# This mixin wraps a headless test terminal. It handles setup, teardown,
|
|
18
|
+
# and provides methods to query buffer content, cursor position, and cell styles.
|
|
19
|
+
#
|
|
20
|
+
# Use it to write fast, deterministic tests for your TUI applications.
|
|
21
|
+
#
|
|
22
|
+
# === Example
|
|
23
|
+
#
|
|
24
|
+
# class MyTest < Minitest::Test
|
|
25
|
+
# include RatatuiRuby::TestHelper
|
|
26
|
+
#
|
|
27
|
+
# def test_rendering
|
|
28
|
+
# with_test_terminal(80, 24) do
|
|
29
|
+
# MyApp.new.run_once
|
|
30
|
+
# assert_includes buffer_content.join, "Hello"
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
module Terminal
|
|
35
|
+
##
|
|
36
|
+
# Initializes a test terminal context with specified dimensions.
|
|
37
|
+
# Restores the original terminal state after the block executes.
|
|
38
|
+
#
|
|
39
|
+
# [width] Integer width of the test terminal (default: 80).
|
|
40
|
+
# [height] Integer height of the test terminal (default: 24).
|
|
41
|
+
# [timeout] Integer maximum execution time in seconds (default: 2). Pass <tt>nil</tt> to disable.
|
|
42
|
+
#
|
|
43
|
+
# === Example
|
|
44
|
+
#
|
|
45
|
+
# with_test_terminal(120, 40) do
|
|
46
|
+
# # render and test your app
|
|
47
|
+
# end
|
|
48
|
+
def with_test_terminal(width = 80, height = 24, **opts)
|
|
49
|
+
RatatuiRuby.init_test_terminal(width, height)
|
|
50
|
+
# Flush any lingering events from previous tests
|
|
51
|
+
while (event = RatatuiRuby.poll_event) && !event.none?; end
|
|
52
|
+
|
|
53
|
+
RatatuiRuby.stub :init_terminal, nil do
|
|
54
|
+
RatatuiRuby.stub :restore_terminal, nil do
|
|
55
|
+
@_ratatui_test_terminal_active = true
|
|
56
|
+
timeout = opts.fetch(:timeout, 2)
|
|
57
|
+
if timeout
|
|
58
|
+
Timeout.timeout(timeout) do
|
|
59
|
+
yield
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
yield
|
|
63
|
+
end
|
|
64
|
+
ensure
|
|
65
|
+
@_ratatui_test_terminal_active = false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
ensure
|
|
69
|
+
RatatuiRuby.restore_terminal
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Current content of the terminal buffer as an array of strings.
|
|
74
|
+
# Each string represents one row.
|
|
75
|
+
#
|
|
76
|
+
# === Example
|
|
77
|
+
#
|
|
78
|
+
# buffer_content
|
|
79
|
+
# # => ["Row 1 text", "Row 2 text", ...]
|
|
80
|
+
def buffer_content
|
|
81
|
+
RatatuiRuby.get_buffer_content.split("\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Current cursor position as a hash with <tt>:x</tt> and <tt>:y</tt> keys.
|
|
86
|
+
#
|
|
87
|
+
# === Example
|
|
88
|
+
#
|
|
89
|
+
# cursor_position
|
|
90
|
+
# # => { x: 0, y: 0 }
|
|
91
|
+
def cursor_position
|
|
92
|
+
x, y = RatatuiRuby.get_cursor_position
|
|
93
|
+
{ x:, y: }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
# Cell attributes at the given coordinates.
|
|
98
|
+
#
|
|
99
|
+
# Returns a hash with <tt>"symbol"</tt>, <tt>"fg"</tt>, and <tt>"bg"</tt> keys.
|
|
100
|
+
#
|
|
101
|
+
# [x] Integer column position (0-indexed).
|
|
102
|
+
# [y] Integer row position (0-indexed).
|
|
103
|
+
#
|
|
104
|
+
# === Example
|
|
105
|
+
#
|
|
106
|
+
# get_cell(0, 0)
|
|
107
|
+
# # => { "symbol" => "H", "fg" => :red, "bg" => nil }
|
|
108
|
+
def get_cell(x, y)
|
|
109
|
+
RatatuiRuby.get_cell_at(x, y)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
# Prints the current buffer to STDOUT with full ANSI colors.
|
|
114
|
+
# Useful for debugging test failures.
|
|
115
|
+
#
|
|
116
|
+
# === Example
|
|
117
|
+
#
|
|
118
|
+
# with_test_terminal do
|
|
119
|
+
# MyApp.new.render
|
|
120
|
+
# print_buffer # see exactly what would display
|
|
121
|
+
# end
|
|
122
|
+
def print_buffer
|
|
123
|
+
puts _render_buffer_with_ansi
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
module TestHelper
|
|
8
|
+
##
|
|
9
|
+
# Test doubles for view testing.
|
|
10
|
+
#
|
|
11
|
+
# View tests verify widget rendering without a real terminal. Real frames draw
|
|
12
|
+
# to the screen. Real rects come from terminal dimensions. Mocking both by hand
|
|
13
|
+
# is tedious.
|
|
14
|
+
#
|
|
15
|
+
# This mixin provides <tt>MockFrame</tt> to capture rendered widgets and
|
|
16
|
+
# <tt>StubRect</tt> to supply fixed dimensions.
|
|
17
|
+
#
|
|
18
|
+
# Use them to test view logic in isolation.
|
|
19
|
+
#
|
|
20
|
+
# === Example
|
|
21
|
+
#
|
|
22
|
+
# frame = MockFrame.new
|
|
23
|
+
# area = StubRect.new(width: 60, height: 20)
|
|
24
|
+
# MyView.new.call(state, tui, frame, area)
|
|
25
|
+
#
|
|
26
|
+
# widget = frame.rendered_widgets.first[:widget]
|
|
27
|
+
# assert_equal "Dashboard", widget.block.title
|
|
28
|
+
module TestDoubles
|
|
29
|
+
##
|
|
30
|
+
# Mock frame for view tests.
|
|
31
|
+
#
|
|
32
|
+
# Captures widgets passed to <tt>render_widget</tt> for later inspection.
|
|
33
|
+
#
|
|
34
|
+
# === Example
|
|
35
|
+
#
|
|
36
|
+
# frame = MockFrame.new
|
|
37
|
+
# View::Log.new.call(state, tui, frame, area)
|
|
38
|
+
# widget = frame.rendered_widgets.first[:widget]
|
|
39
|
+
# assert_equal "Event Log", widget.block.title
|
|
40
|
+
MockFrame = Data.define(:rendered_widgets) do
|
|
41
|
+
def initialize(rendered_widgets: [])
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_widget(widget, area)
|
|
46
|
+
rendered_widgets << { widget:, area: }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Stub rect with fixed dimensions.
|
|
52
|
+
#
|
|
53
|
+
# [x] Integer left edge (default: 0).
|
|
54
|
+
# [y] Integer top edge (default: 0).
|
|
55
|
+
# [width] Integer width in cells (default: 80).
|
|
56
|
+
# [height] Integer height in cells (default: 24).
|
|
57
|
+
#
|
|
58
|
+
# === Example
|
|
59
|
+
#
|
|
60
|
+
# area = StubRect.new(width: 60, height: 20)
|
|
61
|
+
StubRect = Data.define(:x, :y, :width, :height) do
|
|
62
|
+
def initialize(x: 0, y: 0, width: 80, height: 24)
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|