tui-td 0.2.11 → 0.2.13
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/CHANGELOG.md +47 -1
- data/README.md +31 -9
- data/lib/tui_td/ansi_parser.rb +3 -727
- data/lib/tui_td/ansi_utils.rb +3 -73
- data/lib/tui_td/cli.rb +51 -0
- data/lib/tui_td/driver.rb +74 -17
- data/lib/tui_td/matchers.rb +131 -25
- data/lib/tui_td/mcp/server.rb +71 -6
- data/lib/tui_td/selector.rb +31 -0
- data/lib/tui_td/state.rb +2 -123
- data/lib/tui_td/test_runner.rb +101 -19
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +1 -0
- metadata +16 -1
data/lib/tui_td/mcp/server.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Layout/LineLength
|
|
3
|
+
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Layout/LineLength
|
|
4
4
|
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
@@ -60,6 +60,8 @@ module TUITD
|
|
|
60
60
|
@driver&.close
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
ALLOWED_OUTPUT_DIRS = ["/tmp"].freeze
|
|
64
|
+
|
|
63
65
|
private
|
|
64
66
|
|
|
65
67
|
def handle_request(request)
|
|
@@ -282,6 +284,23 @@ module TUITD
|
|
|
282
284
|
required: ["pattern"],
|
|
283
285
|
},
|
|
284
286
|
},
|
|
287
|
+
{
|
|
288
|
+
name: "tui_find_elements",
|
|
289
|
+
description: "Search for UI elements in the terminal state. Returns buttons, checkboxes, dialogs, statusbars, and progress bars detected by heuristic analysis. Optionally filter by role and/or text.",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
role: {
|
|
294
|
+
type: "string",
|
|
295
|
+
description: "Filter by role: button, checkbox, dialog, statusbar, progress. Omit to return all.",
|
|
296
|
+
},
|
|
297
|
+
text: {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: "Filter by visible text (partial match). Optional.",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
285
304
|
{
|
|
286
305
|
name: "tui_close",
|
|
287
306
|
description: "Close the TUI application and clean up the PTY session. Call this when finished.",
|
|
@@ -313,7 +332,8 @@ module TUITD
|
|
|
313
332
|
when "tui_wait_for_exit" then call_tui_wait_for_exit
|
|
314
333
|
when "tui_exit_status" then call_tui_exit_status
|
|
315
334
|
when "tui_find_text" then call_tui_find_text(args)
|
|
316
|
-
when "
|
|
335
|
+
when "tui_find_elements" then call_tui_find_elements(args)
|
|
336
|
+
when "tui_close" then call_tui_close
|
|
317
337
|
else
|
|
318
338
|
return error_response(id, -32_602, "Unknown tool: #{tool_name}")
|
|
319
339
|
end
|
|
@@ -433,7 +453,7 @@ module TUITD
|
|
|
433
453
|
|
|
434
454
|
def call_tui_screenshot(args)
|
|
435
455
|
ensure_driver!
|
|
436
|
-
path = args["path"]
|
|
456
|
+
path = safe_path(args["path"], ext: "png")
|
|
437
457
|
result = @driver.screenshot(path)
|
|
438
458
|
"OK: Screenshot saved to #{result}"
|
|
439
459
|
end
|
|
@@ -444,8 +464,9 @@ module TUITD
|
|
|
444
464
|
renderer = HtmlRenderer.new(@driver.state_data)
|
|
445
465
|
|
|
446
466
|
if path
|
|
447
|
-
|
|
448
|
-
|
|
467
|
+
safe = safe_path(path, ext: "html")
|
|
468
|
+
renderer.render(safe)
|
|
469
|
+
"OK: HTML saved to #{safe}"
|
|
449
470
|
else
|
|
450
471
|
renderer.to_html
|
|
451
472
|
end
|
|
@@ -485,6 +506,39 @@ module TUITD
|
|
|
485
506
|
end
|
|
486
507
|
end
|
|
487
508
|
|
|
509
|
+
def call_tui_find_elements(args)
|
|
510
|
+
ensure_driver!
|
|
511
|
+
state = TUITD::State.new(@driver.state_data)
|
|
512
|
+
selector = TUITD::Selector.new(state)
|
|
513
|
+
|
|
514
|
+
role = args["role"]&.to_sym
|
|
515
|
+
text = args["text"]
|
|
516
|
+
|
|
517
|
+
elements = if role
|
|
518
|
+
selector.get_by_role(role)
|
|
519
|
+
else
|
|
520
|
+
selector.elements
|
|
521
|
+
end
|
|
522
|
+
elements = elements.select { |e| e.text&.include?(text) } if text
|
|
523
|
+
|
|
524
|
+
if elements.empty?
|
|
525
|
+
desc = role ? "role :#{role}" : "any role"
|
|
526
|
+
desc += " with text #{text.inspect}" if text
|
|
527
|
+
"No elements found for #{desc}"
|
|
528
|
+
else
|
|
529
|
+
lines = ["Found #{elements.size} element(s):"]
|
|
530
|
+
elements.each do |el|
|
|
531
|
+
parts = [" :#{el.role}"]
|
|
532
|
+
parts << el.text.inspect if el.text
|
|
533
|
+
parts << "at [#{el.row},#{el.col}]"
|
|
534
|
+
parts << "#{el.width}x#{el.height}"
|
|
535
|
+
parts << "(checked)" if el.checked
|
|
536
|
+
lines << parts.join(" ")
|
|
537
|
+
end
|
|
538
|
+
lines.join("\n")
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
488
542
|
def call_tui_close
|
|
489
543
|
@driver&.close
|
|
490
544
|
@driver = nil
|
|
@@ -493,6 +547,17 @@ module TUITD
|
|
|
493
547
|
|
|
494
548
|
# --- Helpers ---
|
|
495
549
|
|
|
550
|
+
def safe_path(user_path, ext:)
|
|
551
|
+
default = File.join("/tmp", "tui_td_#{Time.now.to_i}.#{ext}")
|
|
552
|
+
resolved = File.expand_path(user_path || default)
|
|
553
|
+
|
|
554
|
+
unless ALLOWED_OUTPUT_DIRS.any? { |dir| resolved.start_with?(File.expand_path(dir)) }
|
|
555
|
+
raise TUITD::Error, "Output path must be under one of: #{ALLOWED_OUTPUT_DIRS.join(", ")}"
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
resolved
|
|
559
|
+
end
|
|
560
|
+
|
|
496
561
|
def ensure_driver!
|
|
497
562
|
raise Error, "No TUI session active. Call tui_start first." if @driver.nil?
|
|
498
563
|
end
|
|
@@ -530,4 +595,4 @@ module TUITD
|
|
|
530
595
|
end
|
|
531
596
|
end
|
|
532
597
|
end
|
|
533
|
-
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Layout/LineLength
|
|
598
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Layout/LineLength
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tans-parser"
|
|
4
|
+
|
|
5
|
+
module TUITD
|
|
6
|
+
Selector = TansParser::Selector
|
|
7
|
+
Element = TansParser::Element
|
|
8
|
+
|
|
9
|
+
# Extend the aliased Selector with within scoping.
|
|
10
|
+
# within is a testing-specific pattern (scope queries to a dialog region)
|
|
11
|
+
# and therefore lives in tui-td rather than tans-parser.
|
|
12
|
+
Selector.class_eval do
|
|
13
|
+
# Return a new Selector whose elements are filtered to the given bounding box.
|
|
14
|
+
# Coordinates of returned elements are relative to the box origin.
|
|
15
|
+
def within(top_row, left_col, width, height)
|
|
16
|
+
scoped = @elements.select do |e|
|
|
17
|
+
e.row >= top_row && e.row < top_row + height &&
|
|
18
|
+
e.col >= left_col && e.col < left_col + width
|
|
19
|
+
end
|
|
20
|
+
scoped.each do |e|
|
|
21
|
+
e = e.dup
|
|
22
|
+
e.row -= top_row
|
|
23
|
+
e.col -= left_col
|
|
24
|
+
end
|
|
25
|
+
self.class.allocate.tap do |s|
|
|
26
|
+
s.instance_variable_set(:@state, @state)
|
|
27
|
+
s.instance_variable_set(:@elements, scoped)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/tui_td/state.rb
CHANGED
|
@@ -1,128 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "tans-parser"
|
|
4
4
|
|
|
5
5
|
module TUITD
|
|
6
|
-
|
|
7
|
-
# Provides high-level query methods for AI consumption.
|
|
8
|
-
class State
|
|
9
|
-
attr_reader :rows, :cols, :grid, :cursor, :cursor_visible, :cursor_style, :mouse_mode, :mouse_format
|
|
10
|
-
|
|
11
|
-
def initialize(data)
|
|
12
|
-
raise ArgumentError, "State data must include :size key" unless data[:size]
|
|
13
|
-
raise ArgumentError, "State data must include :rows key" unless data[:rows]
|
|
14
|
-
|
|
15
|
-
@rows = data[:size][:rows]
|
|
16
|
-
@cols = data[:size][:cols]
|
|
17
|
-
@grid = data[:rows]
|
|
18
|
-
@cursor = data[:cursor]
|
|
19
|
-
|
|
20
|
-
cursor_info = data[:cursor].is_a?(Hash) ? data[:cursor] : {}
|
|
21
|
-
@cursor_visible = data.key?(:cursor_visible) ? data[:cursor_visible] : (cursor_info[:visible] != false)
|
|
22
|
-
@cursor_style = data.key?(:cursor_style) ? data[:cursor_style] : (cursor_info[:style] || 1)
|
|
23
|
-
|
|
24
|
-
@mouse_mode = data[:mouse_mode] || :none
|
|
25
|
-
@mouse_format = data[:mouse_format] || :normal
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Get plain text of the entire terminal (no ANSI)
|
|
29
|
-
def plain_text
|
|
30
|
-
@grid.map { |row| row.map { |c| c[:char] }.join.rstrip }.join("\n")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Get text at a specific position
|
|
34
|
-
def text_at(row, col, length = @cols - col)
|
|
35
|
-
return "" if row >= @rows || col >= @cols
|
|
36
|
-
|
|
37
|
-
@grid[row][col, length].map { |c| c[:char] }.join
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Search for text across the entire terminal
|
|
41
|
-
def find_text(pattern)
|
|
42
|
-
results = []
|
|
43
|
-
@grid.each_with_index do |row, ri|
|
|
44
|
-
text = row.map { |c| c[:char] }.join
|
|
45
|
-
pos = 0
|
|
46
|
-
while (match = text.index(pattern, pos))
|
|
47
|
-
results << { row: ri, col: match, text: pattern, full_line: text }
|
|
48
|
-
pos = match + 1
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
results
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Get the color at a specific cell
|
|
55
|
-
def foreground_at(row, col)
|
|
56
|
-
return nil if row >= @rows || col >= @cols
|
|
57
|
-
|
|
58
|
-
@grid[row][col][:fg]
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def background_at(row, col)
|
|
62
|
-
return nil if row >= @rows || col >= @cols
|
|
63
|
-
|
|
64
|
-
@grid[row][col][:bg]
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def style_at(row, col)
|
|
68
|
-
return nil if row >= @rows || col >= @cols
|
|
69
|
-
|
|
70
|
-
cell = @grid[row][col]
|
|
71
|
-
{ bold: cell[:bold], italic: cell[:italic], underline: cell[:underline] }
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def to_ai_json
|
|
75
|
-
h = extract_highlights
|
|
76
|
-
cursor_info = @cursor.is_a?(Hash) ? @cursor : {}
|
|
77
|
-
r = cursor_info[:row] || cursor_info["row"] || 0
|
|
78
|
-
c = cursor_info[:col] || cursor_info["col"] || 0
|
|
79
|
-
styled_count = h.count { |hl| hl[:bold] || hl[:italic] || hl[:underline] || hl[:fg] || hl[:bg] }
|
|
80
|
-
|
|
81
|
-
summary = "Cursor at [#{r},#{c}]. "
|
|
82
|
-
summary << "#{styled_count} styled row#{"s" unless styled_count == 1}"
|
|
83
|
-
fgs = h.flat_map { |hl| hl[:fg] }.compact.uniq
|
|
84
|
-
bgs = h.flat_map { |hl| hl[:bg] }.compact.uniq
|
|
85
|
-
summary << ", colors: fg=#{fgs.sort.join(",")}" unless fgs.empty?
|
|
86
|
-
summary << ", bg=#{bgs.sort.join(",")}" unless bgs.empty?
|
|
87
|
-
summary << "."
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
size: { rows: @rows, cols: @cols },
|
|
91
|
-
cursor: cursor_info,
|
|
92
|
-
text: plain_text,
|
|
93
|
-
highlights: h,
|
|
94
|
-
summary: summary,
|
|
95
|
-
}
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
private
|
|
99
|
-
|
|
100
|
-
def extract_highlights
|
|
101
|
-
highlights = []
|
|
102
|
-
@grid.each_with_index do |row, ri|
|
|
103
|
-
row_text = row.map { |c| c[:char] }.join
|
|
104
|
-
next if row_text.strip.empty?
|
|
105
|
-
|
|
106
|
-
fgs = row.map { |c| c[:fg] || c["fg"] || "default" }
|
|
107
|
-
.uniq.reject { |c| c == "default" }
|
|
108
|
-
bgs = row.map { |c| c[:bg] || c["bg"] || "default" }
|
|
109
|
-
.uniq.reject { |c| c == "default" }
|
|
110
|
-
bold = row.any? { |c| c[:bold] || c["bold"] }
|
|
111
|
-
italic = row.any? { |c| c[:italic] || c["italic"] }
|
|
112
|
-
underline = row.any? { |c| c[:underline] || c["underline"] }
|
|
113
|
-
|
|
114
|
-
next if fgs.empty? && bgs.empty? && !bold && !italic && !underline
|
|
115
|
-
|
|
116
|
-
h = { row: ri, text: row_text }
|
|
117
|
-
h[:bold] = true if bold
|
|
118
|
-
h[:italic] = true if italic
|
|
119
|
-
h[:underline] = true if underline
|
|
120
|
-
h[:fg] = fgs.size == 1 ? fgs.first : fgs unless fgs.empty?
|
|
121
|
-
h[:bg] = bgs.size == 1 ? bgs.first : bgs unless bgs.empty?
|
|
122
|
-
highlights << h
|
|
123
|
-
end
|
|
124
|
-
highlights
|
|
125
|
-
end
|
|
126
|
-
end
|
|
6
|
+
State = TansParser::State
|
|
127
7
|
end
|
|
128
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
data/lib/tui_td/test_runner.rb
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
3
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength, Metrics/ClassLength
|
|
4
4
|
|
|
5
5
|
require "json"
|
|
6
|
+
require "shellwords"
|
|
6
7
|
|
|
7
8
|
module TUITD
|
|
8
9
|
# Executes TUI tests defined in JSON format.
|
|
@@ -108,29 +109,41 @@ module TUITD
|
|
|
108
109
|
when "assert_style"
|
|
109
110
|
ensure_driver!(driver)
|
|
110
111
|
row, col = coords(step)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
expected_style = {}
|
|
113
|
+
expected_style[:bold] = step[:bold] unless step[:bold].nil?
|
|
114
|
+
expected_style[:italic] = step[:italic] unless step[:italic].nil?
|
|
115
|
+
expected_style[:underline] = step[:underline] unless step[:underline].nil?
|
|
116
|
+
actual = nil
|
|
117
|
+
match = begin
|
|
118
|
+
driver.wait_for(timeout: 2) do |s|
|
|
119
|
+
actual = s.style_at(row, col)
|
|
120
|
+
expected_style.all? { |k, v| actual[k] == v }
|
|
121
|
+
end
|
|
122
|
+
true
|
|
123
|
+
rescue TimeoutError
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
actual ||= begin
|
|
127
|
+
state = State.new(driver.state_data)
|
|
128
|
+
state.style_at(row, col)
|
|
129
|
+
end
|
|
118
130
|
if match
|
|
119
|
-
Result.new(step: action, passed: true,
|
|
131
|
+
Result.new(step: action, passed: true,
|
|
132
|
+
message: "Style at [#{row},#{col}] matches #{expected_style}",)
|
|
120
133
|
else
|
|
121
134
|
Result.new(step: action, passed: false,
|
|
122
|
-
message: "Style at [#{row},#{col}] is #{actual}, expected #{
|
|
135
|
+
message: "Style at [#{row},#{col}] is #{actual}, expected #{expected_style}",)
|
|
123
136
|
end
|
|
124
137
|
|
|
125
138
|
when "screenshot"
|
|
126
139
|
ensure_driver!(driver)
|
|
127
|
-
path =
|
|
140
|
+
path = safe_output_path(value, "png")
|
|
128
141
|
driver.screenshot(path)
|
|
129
142
|
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
130
143
|
|
|
131
144
|
when "html"
|
|
132
145
|
ensure_driver!(driver)
|
|
133
|
-
path =
|
|
146
|
+
path = safe_output_path(value, "html")
|
|
134
147
|
HtmlRenderer.new(driver.state_data).render(path)
|
|
135
148
|
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
136
149
|
|
|
@@ -150,6 +163,19 @@ module TUITD
|
|
|
150
163
|
Result.new(step: action, passed: false, message: "Exit status #{actual}, expected #{expected}")
|
|
151
164
|
end
|
|
152
165
|
|
|
166
|
+
when "assert_button"
|
|
167
|
+
check_role(driver, :button, value.to_s)
|
|
168
|
+
|
|
169
|
+
when "assert_dialog"
|
|
170
|
+
check_role(driver, :dialog, nil)
|
|
171
|
+
|
|
172
|
+
when "assert_checkbox"
|
|
173
|
+
check_role(driver, :checkbox, value.to_s, checked: step[:checked])
|
|
174
|
+
|
|
175
|
+
when "assert_role"
|
|
176
|
+
role = step[:role]&.to_sym
|
|
177
|
+
check_role(driver, role, value.to_s)
|
|
178
|
+
|
|
153
179
|
when "close"
|
|
154
180
|
driver&.close
|
|
155
181
|
driver = nil
|
|
@@ -194,8 +220,40 @@ module TUITD
|
|
|
194
220
|
}
|
|
195
221
|
end
|
|
196
222
|
|
|
223
|
+
ALLOWED_OUTPUT_DIRS = ["/tmp"].freeze
|
|
224
|
+
|
|
197
225
|
private
|
|
198
226
|
|
|
227
|
+
def check_role(driver, role, text, checked: nil)
|
|
228
|
+
ensure_driver!(driver)
|
|
229
|
+
state = State.new(driver.state_data)
|
|
230
|
+
selector = Selector.new(state)
|
|
231
|
+
elements = selector.get_by_role(role)
|
|
232
|
+
elements = elements.select { |e| e.text&.include?(text.to_s) } if text
|
|
233
|
+
elements = elements.select { |e| e.checked == checked } unless checked.nil?
|
|
234
|
+
|
|
235
|
+
action = "assert_#{role}"
|
|
236
|
+
if elements.any?
|
|
237
|
+
count = elements.size
|
|
238
|
+
desc = text ? "#{role} #{text.inspect}" : role.to_s
|
|
239
|
+
Result.new(step: action, passed: true, message: "Found #{count} #{desc} element(s)")
|
|
240
|
+
else
|
|
241
|
+
desc = text ? "#{role} with text #{text.inspect}" : role.to_s
|
|
242
|
+
Result.new(step: action, passed: false, message: "No #{desc} found")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def safe_output_path(value, ext)
|
|
247
|
+
default = File.join("/tmp", "tui_td_#{Time.now.to_i}.#{ext}")
|
|
248
|
+
resolved = File.expand_path(value.is_a?(String) ? value : default)
|
|
249
|
+
|
|
250
|
+
unless ALLOWED_OUTPUT_DIRS.any? { |dir| resolved.start_with?(File.expand_path(dir)) }
|
|
251
|
+
raise TUITD::Error, "Output path must be under one of: #{ALLOWED_OUTPUT_DIRS.join(", ")}"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
resolved
|
|
255
|
+
end
|
|
256
|
+
|
|
199
257
|
def ensure_driver!(driver)
|
|
200
258
|
raise Error, "No session. Add a 'start' step first." if driver.nil?
|
|
201
259
|
end
|
|
@@ -209,7 +267,6 @@ module TUITD
|
|
|
209
267
|
|
|
210
268
|
def check_text(driver, value, action)
|
|
211
269
|
ensure_driver!(driver)
|
|
212
|
-
state = State.new(driver.state_data)
|
|
213
270
|
text = value.to_s
|
|
214
271
|
|
|
215
272
|
if action == "assert_regex"
|
|
@@ -222,7 +279,17 @@ module TUITD
|
|
|
222
279
|
pattern = text
|
|
223
280
|
end
|
|
224
281
|
|
|
225
|
-
found =
|
|
282
|
+
found = if action == "assert_not_text"
|
|
283
|
+
state = State.new(driver.state_data)
|
|
284
|
+
state.find_text(pattern).any?
|
|
285
|
+
else
|
|
286
|
+
begin
|
|
287
|
+
driver.wait_for(timeout: 2) { |s| s.find_text(pattern).any? }
|
|
288
|
+
true
|
|
289
|
+
rescue TimeoutError
|
|
290
|
+
false
|
|
291
|
+
end
|
|
292
|
+
end
|
|
226
293
|
|
|
227
294
|
case action
|
|
228
295
|
when "assert_text"
|
|
@@ -252,17 +319,32 @@ module TUITD
|
|
|
252
319
|
ensure_driver!(driver)
|
|
253
320
|
row, col = coords(step)
|
|
254
321
|
expected = step[:is] || step["is"]
|
|
255
|
-
state = State.new(driver.state_data)
|
|
256
322
|
label = property == :fg ? "FG" : "BG"
|
|
257
|
-
actual = property == :fg ? state.foreground_at(row, col) : state.background_at(row, col)
|
|
258
323
|
|
|
259
|
-
|
|
324
|
+
actual_val = nil
|
|
325
|
+
found = begin
|
|
326
|
+
driver.wait_for(timeout: 2) do |s|
|
|
327
|
+
actual_val = property == :fg ? s.foreground_at(row, col) : s.background_at(row, col)
|
|
328
|
+
actual_val == expected
|
|
329
|
+
end
|
|
330
|
+
true
|
|
331
|
+
rescue TimeoutError
|
|
332
|
+
# actual_val holds the last observed value
|
|
333
|
+
false
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
actual_val ||= begin
|
|
337
|
+
state = State.new(driver.state_data)
|
|
338
|
+
property == :fg ? state.foreground_at(row, col) : state.background_at(row, col)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
if found
|
|
260
342
|
Result.new(step: step.keys.first.to_s, passed: true, message: "#{label} at [#{row},#{col}] is #{expected}")
|
|
261
343
|
else
|
|
262
344
|
Result.new(step: step.keys.first.to_s, passed: false,
|
|
263
|
-
message: "#{label} at [#{row},#{col}] is #{
|
|
345
|
+
message: "#{label} at [#{row},#{col}] is #{actual_val}, expected #{expected}",)
|
|
264
346
|
end
|
|
265
347
|
end
|
|
266
348
|
end
|
|
267
349
|
end
|
|
268
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
350
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength, Metrics/ClassLength
|
data/lib/tui_td/version.rb
CHANGED
data/lib/tui_td.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "tui_td/state"
|
|
|
16
16
|
require_relative "tui_td/screenshot"
|
|
17
17
|
require_relative "tui_td/html_renderer"
|
|
18
18
|
require_relative "tui_td/test_runner"
|
|
19
|
+
require_relative "tui_td/selector"
|
|
19
20
|
require_relative "tui_td/mcp/server"
|
|
20
21
|
require_relative "tui_td/cli"
|
|
21
22
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tui-td
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haluk Durmus
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: tans-parser
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.1'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.1'
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
83
|
name: bundler-audit
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -159,6 +173,7 @@ files:
|
|
|
159
173
|
- lib/tui_td/matchers.rb
|
|
160
174
|
- lib/tui_td/mcp/server.rb
|
|
161
175
|
- lib/tui_td/screenshot.rb
|
|
176
|
+
- lib/tui_td/selector.rb
|
|
162
177
|
- lib/tui_td/state.rb
|
|
163
178
|
- lib/tui_td/test_runner.rb
|
|
164
179
|
- lib/tui_td/unifont_glyphs.rb
|