tui-td 0.2.12 → 0.2.14
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 +35 -0
- data/README.md +31 -9
- data/lib/tui_td/cli.rb +51 -0
- data/lib/tui_td/driver.rb +63 -15
- data/lib/tui_td/matchers.rb +131 -25
- data/lib/tui_td/mcp/server.rb +54 -3
- data/lib/tui_td/selector.rb +31 -0
- data/lib/tui_td/test_runner.rb +83 -15
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 81e8cf766200bdb7f86334be97036e68f36df7e352618e823185976ebb07a629
|
|
4
|
+
data.tar.gz: 1632047e8e7b668d0aee6fcf3b14ff16b01d5c23d4cca1f126315d6fe8c1606b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0f08cd1cc35c05466f5f5b77f45f674635d390db8a68bf9a75e316e2cc70f681daf718211a8563b765ccbfe1b7689d38e28378311dc2ad9afa3bf57a946a172
|
|
7
|
+
data.tar.gz: 5f6385932f9c350c3186dfe44a3c47bc0c169c86189feb90c8a54dc6ddfdb54f0b97489ffe812da557cc2e6828b384b28cd81ca286be977221586c4f0788fe33
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Tighten tans-parser dependency from `~> 0.1` to `~> 0.1.1` to ensure the required
|
|
8
|
+
Selector/Element classes are present (0.1.0 lacks them)
|
|
9
|
+
|
|
10
|
+
## 0.2.13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Auto-wait mechanisms: `Driver#wait_for(predicate)` with adaptive polling (10ms → 100ms),
|
|
15
|
+
auto-wait on RSpec matchers (3s timeout when given a Driver), auto-wait on JSON test
|
|
16
|
+
assertions (2s per-check timeout)
|
|
17
|
+
- Semantic selectors: `Element` struct and `Selector` class with heuristic role detection
|
|
18
|
+
for buttons, checkboxes, dialogs, statusbars, and progress bars
|
|
19
|
+
- RSpec matchers: `have_button`, `have_dialog`, `have_checkbox`, `have_role`
|
|
20
|
+
- JSON test steps: `assert_button`, `assert_dialog`, `assert_checkbox`, `assert_role`
|
|
21
|
+
- `within` scoping for filtering elements by bounding box
|
|
22
|
+
- `poll_interval` parameter on Driver for configurable polling speed
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `wait_for_stable` uses buffer-size tracking instead of full grid parse for performance
|
|
27
|
+
- Output buffer capped at 10 MB (ring buffer) to prevent unbounded memory growth
|
|
28
|
+
- Delegate `Selector` and `Element` to tans-parser 0.1.1
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
- Add CONTRIBUTING.md with development setup, code quality, and PR workflow
|
|
33
|
+
- Add docs/quick_start.md with 2-minute getting-started tutorial
|
|
34
|
+
- Add docs/faq.md covering 8 common troubleshooting topics
|
|
35
|
+
- Add whiptail dialog example (`examples/whiptail_dialog.json`)
|
|
36
|
+
- Update CLI help (`tui-td help test`, `tui-td help rspec`) with new steps and matchers
|
|
37
|
+
|
|
3
38
|
## 0.2.12
|
|
4
39
|
|
|
5
40
|
### Security
|
data/README.md
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
# TUI Test Drive
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/tui-td)
|
|
4
|
+
[](LICENSE.txt)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
Testing framework for Terminal User Interfaces (TUIs). Start a TUI in a PTY, send input, analyze output — as structured JSON, plain text, PNG screenshots, or HTML renders. Includes an MCP server for AI-driven testing, auto-wait RSpec matchers, and semantic selectors.
|
|
7
|
+
|
|
8
|
+
> New to tui-td? Jump to [Quick Start](docs/quick_start.md).
|
|
9
|
+
|
|
10
|
+
**What tui-td gives you:**
|
|
11
|
+
|
|
12
|
+
1. **Start any TUI** in a virtual terminal (PTY) with `Driver` or JSON test plans
|
|
13
|
+
2. **Auto-wait assertions** — matchers automatically retry until the condition is met or timeout
|
|
14
|
+
3. **Semantic selectors** — `get_by_role(:button)`, `get_by_role(:dialog)`, `within { }` scoping
|
|
15
|
+
4. **Multiple output formats** — structured JSON, plain text, PNG screenshots, HTML renders
|
|
16
|
+
5. **JSON test runner** — language-agnostic, 15+ step types, CI-friendly
|
|
17
|
+
6. **RSpec matchers** — `have_text`, `have_fg`, `have_button`, `have_dialog`, and more
|
|
18
|
+
7. **MCP server** — AI agents can drive TUIs via JSON-RPC over stdio
|
|
19
|
+
8. **Pure Ruby rendering** — embedded Spleen font + 2766 Unifont glyphs, no native deps required
|
|
11
20
|
|
|
12
21
|
## Installation
|
|
13
22
|
|
|
@@ -83,6 +92,7 @@ Examples:
|
|
|
83
92
|
Interactive commands (drive mode):
|
|
84
93
|
state Show terminal state as pretty JSON
|
|
85
94
|
raw Show raw ANSI output
|
|
95
|
+
elements Show detected UI elements (buttons, dialogs, etc.)
|
|
86
96
|
key <name> Send keystroke (enter, tab, escape, up, down, left, right,
|
|
87
97
|
backspace, ctrl_c, ctrl_d)
|
|
88
98
|
<text> Send text to the TUI
|
|
@@ -273,6 +283,10 @@ tui-td test examples/echo_test.json
|
|
|
273
283
|
| `assert_exit` | `N` | Assert the process exit code equals N |
|
|
274
284
|
| `screenshot` | `"path"` | Save PNG screenshot |
|
|
275
285
|
| `html` | `"path"` | Save HTML render for browser viewing |
|
|
286
|
+
| `assert_button` | `"text"` | Assert a button with given text is visible (`[ OK ]`, `(Cancel)`, `<Submit>`) |
|
|
287
|
+
| `assert_dialog` | — | Assert a dialog (box-drawing region) is visible |
|
|
288
|
+
| `assert_checkbox` | `"label", "checked": true` | Assert a checkbox with given label (and optionally checked state) |
|
|
289
|
+
| `assert_role` | `":button", "text": "OK"` | Generic role assertion (`:button`, `:checkbox`, `:dialog`, `:statusbar`, `:progress`) |
|
|
276
290
|
| `close` | — | Close the TUI |
|
|
277
291
|
|
|
278
292
|
Example with `html` step for before/after snapshots:
|
|
@@ -347,6 +361,10 @@ end
|
|
|
347
361
|
| `have_fg("color").at(row, col)` | Assert foreground color at position |
|
|
348
362
|
| `have_bg("color").at(row, col)` | Assert background color at position |
|
|
349
363
|
| `have_style.at(row, col).with(bold: true, ...)` | Assert cell style |
|
|
364
|
+
| `have_button("OK")` | Assert a button with given text is visible |
|
|
365
|
+
| `have_dialog` | Assert a dialog (box-drawing region) is visible |
|
|
366
|
+
| `have_checkbox("Label").checked` | Assert a checkbox with given label (chain `.checked`) |
|
|
367
|
+
| `have_role(:button, text: "OK")` | Generic role assertion with optional text filter |
|
|
350
368
|
| `have_exit_status(N)` | Assert the driver process exit status equals N |
|
|
351
369
|
|
|
352
370
|
## MCP Server — AI Integration
|
|
@@ -373,6 +391,7 @@ tui-td serve
|
|
|
373
391
|
| `tui_wait_for_exit` | Wait until the TUI process exits. Returns exit status. |
|
|
374
392
|
| `tui_exit_status` | Get the exit status code (nil if still running). |
|
|
375
393
|
| `tui_find_text` | Search for text or regex in terminal state. Returns positions of all matches. |
|
|
394
|
+
| `tui_find_elements` | Detect UI elements (buttons, checkboxes, dialogs, etc.) with optional role/text filters. |
|
|
376
395
|
| `tui_close` | Close the TUI and clean up. |
|
|
377
396
|
|
|
378
397
|
### MCP configuration
|
|
@@ -419,10 +438,13 @@ Add to your MCP client configuration:
|
|
|
419
438
|
// 8. Search for text in the terminal
|
|
420
439
|
{"method": "tools/call", "params": {"name": "tui_find_text", "arguments": {"pattern": "error|fail"}}}
|
|
421
440
|
|
|
422
|
-
// 9.
|
|
441
|
+
// 9. Find UI elements by role
|
|
442
|
+
{"method": "tools/call", "params": {"name": "tui_find_elements", "arguments": {"role": "button"}}}
|
|
443
|
+
|
|
444
|
+
// 10. Check exit status (or wait for exit)
|
|
423
445
|
{"method": "tools/call", "params": {"name": "tui_exit_status", "arguments": {}}}
|
|
424
446
|
|
|
425
|
-
//
|
|
447
|
+
// 11. Clean up
|
|
426
448
|
{"method": "tools/call", "params": {"name": "tui_close", "arguments": {}}}
|
|
427
449
|
```
|
|
428
450
|
|
data/lib/tui_td/cli.rb
CHANGED
|
@@ -40,6 +40,7 @@ module TUITD
|
|
|
40
40
|
opts.separator "Interactive commands (drive mode):"
|
|
41
41
|
opts.separator " state Show terminal state as pretty JSON"
|
|
42
42
|
opts.separator " raw Show raw ANSI output"
|
|
43
|
+
opts.separator " elements Show detected UI elements (buttons, dialogs, etc.)"
|
|
43
44
|
opts.separator " key <name> Send keystroke (enter, tab, escape, up, down, left, right,"
|
|
44
45
|
opts.separator " backspace, ctrl_c, ctrl_d)"
|
|
45
46
|
opts.separator " <text> Send text to the TUI"
|
|
@@ -195,6 +196,14 @@ module TUITD
|
|
|
195
196
|
elsif input == "exitstatus"
|
|
196
197
|
status = driver.exitstatus
|
|
197
198
|
puts status ? "Exit status: #{status}" : "Process still running"
|
|
199
|
+
elsif input == "elements"
|
|
200
|
+
state = State.new(driver.state_data)
|
|
201
|
+
selector = Selector.new(state)
|
|
202
|
+
puts "Buttons: #{selector.buttons.map { |e| "#{e.text}@[#{e.row},#{e.col}]" }.join(", ")}"
|
|
203
|
+
puts "Checkboxes: #{selector.checkboxes.map { |e| "#{e.text} (#{e.checked ? "✓" : "☐"})" }.join(", ")}"
|
|
204
|
+
puts "Dialogs: #{selector.dialogs.map { |e| "\"#{e.text}\" #{e.width}x#{e.height}" }.join(", ")}"
|
|
205
|
+
puts "Statusbars: #{selector.statusbars.map(&:text).join(", ")}"
|
|
206
|
+
puts "Progress: #{selector.progress_bars.map(&:text).join(", ")}"
|
|
198
207
|
elsif input.start_with?("key ")
|
|
199
208
|
driver.send_keys(input.split(" ", 2).last.to_sym)
|
|
200
209
|
else
|
|
@@ -410,6 +419,22 @@ module TUITD
|
|
|
410
419
|
{"close": true}
|
|
411
420
|
Close the driver session (force-kill if needed).
|
|
412
421
|
|
|
422
|
+
{"assert_button": "<text>"}
|
|
423
|
+
Find a button with the given text. Buttons are detected
|
|
424
|
+
by patterns like [ OK ], (Cancel), <Submit>.
|
|
425
|
+
|
|
426
|
+
{"assert_dialog": true}
|
|
427
|
+
Assert that at least one dialog (box-drawing region) is visible.
|
|
428
|
+
|
|
429
|
+
{"assert_checkbox": "<text>", "checked": true}
|
|
430
|
+
Find a checkbox with the given label text. Optional "checked"
|
|
431
|
+
(true/false) to match checked state. Detects [x], [*], [ ] at
|
|
432
|
+
line starts.
|
|
433
|
+
|
|
434
|
+
{"assert_role": ":button", "text": "OK"}
|
|
435
|
+
Generic role assertion. Accepts :button, :checkbox, :dialog,
|
|
436
|
+
:statusbar, :progress. Optional "text" filter.
|
|
437
|
+
|
|
413
438
|
Example test file: examples/echo_test.json
|
|
414
439
|
HELP
|
|
415
440
|
exit 0
|
|
@@ -470,6 +495,32 @@ module TUITD
|
|
|
470
495
|
Assert style attributes at [row, col] match the given hash.
|
|
471
496
|
Usage: expect(state).to have_style.at(0, 0).with(bold: true)
|
|
472
497
|
|
|
498
|
+
Selector matchers (semantic UI element detection)
|
|
499
|
+
-------------------------------------------------
|
|
500
|
+
|
|
501
|
+
These matchers detect UI elements by their visual appearance:
|
|
502
|
+
|
|
503
|
+
have_button("OK")
|
|
504
|
+
Passes if a button with the given text is visible.
|
|
505
|
+
Detects [ OK ], (Cancel), <Submit> patterns.
|
|
506
|
+
Usage: expect(state).to have_button("OK")
|
|
507
|
+
|
|
508
|
+
have_dialog
|
|
509
|
+
Passes if a dialog (box-drawing character region) is visible.
|
|
510
|
+
Usage: expect(state).to have_dialog
|
|
511
|
+
|
|
512
|
+
have_checkbox("Enable").checked
|
|
513
|
+
Passes if a checkbox with the given label is visible.
|
|
514
|
+
Chain .checked to require the box to be checked.
|
|
515
|
+
Detects [x], [*], [ ] at line starts.
|
|
516
|
+
Usage: expect(state).to have_checkbox("Enable logging")
|
|
517
|
+
Usage: expect(state).to have_checkbox("Auto-save").checked
|
|
518
|
+
|
|
519
|
+
have_role(:button, text: "OK")
|
|
520
|
+
Generic role matcher. Accepts :button, :checkbox, :dialog,
|
|
521
|
+
:statusbar, :progress. Optional text: filter.
|
|
522
|
+
Usage: expect(state).to have_role(:statusbar)
|
|
523
|
+
|
|
473
524
|
Driver matchers (work on TUITD::Driver, not State)
|
|
474
525
|
--------------------------------------------------
|
|
475
526
|
|
data/lib/tui_td/driver.rb
CHANGED
|
@@ -26,7 +26,9 @@ module TUITD
|
|
|
26
26
|
|
|
27
27
|
attr_reader :command, :state
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
MAX_BUFFER_SIZE = 10 * 1024 * 1024 # 10 MB ring buffer
|
|
30
|
+
|
|
31
|
+
def initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil, env: {}, poll_interval: nil)
|
|
30
32
|
@command = command
|
|
31
33
|
@rows = rows
|
|
32
34
|
@cols = cols
|
|
@@ -41,6 +43,7 @@ module TUITD
|
|
|
41
43
|
@output_mutex = Mutex.new
|
|
42
44
|
@reader_thread = nil
|
|
43
45
|
@reader_running = false
|
|
46
|
+
@poll_interval = poll_interval
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
# Start the TUI application in a PTY
|
|
@@ -92,9 +95,34 @@ module TUITD
|
|
|
92
95
|
end
|
|
93
96
|
end
|
|
94
97
|
|
|
98
|
+
# Wait until the predicate returns true for the current terminal state.
|
|
99
|
+
# Polls with adaptive intervals: 10ms → 25ms → 50ms → 100ms.
|
|
100
|
+
# Use a custom poll_interval to bypass adaptive behavior.
|
|
101
|
+
#
|
|
102
|
+
# driver.wait_for { |state| state.find_text("Ready").any? }
|
|
103
|
+
# driver.wait_for(timeout: 5) { |state| state.foreground_at(0, 0) == "green" }
|
|
104
|
+
#
|
|
105
|
+
def wait_for(timeout: nil, &predicate)
|
|
106
|
+
deadline = monotonic + (timeout || @timeout)
|
|
107
|
+
loop_count = 0
|
|
108
|
+
loop do
|
|
109
|
+
raise TimeoutError, "Timeout waiting for predicate" if monotonic > deadline
|
|
110
|
+
|
|
111
|
+
read_available!
|
|
112
|
+
refresh_state!
|
|
113
|
+
state_obj = State.new(@state)
|
|
114
|
+
break if predicate.call(state_obj)
|
|
115
|
+
|
|
116
|
+
adaptive_sleep(loop_count)
|
|
117
|
+
loop_count += 1
|
|
118
|
+
end
|
|
119
|
+
@state
|
|
120
|
+
end
|
|
121
|
+
|
|
95
122
|
# Wait until output contains the given text
|
|
96
123
|
def wait_for_text(text)
|
|
97
124
|
deadline = monotonic + @timeout
|
|
125
|
+
loop_count = 0
|
|
98
126
|
loop do
|
|
99
127
|
raise TimeoutError, "Timeout waiting for: #{text.inspect}" if monotonic > deadline
|
|
100
128
|
|
|
@@ -102,7 +130,8 @@ module TUITD
|
|
|
102
130
|
found = @output_mutex.synchronize { @output_buffer.include?(text) }
|
|
103
131
|
break if found
|
|
104
132
|
|
|
105
|
-
|
|
133
|
+
adaptive_sleep(loop_count)
|
|
134
|
+
loop_count += 1
|
|
106
135
|
end
|
|
107
136
|
refresh_state!
|
|
108
137
|
end
|
|
@@ -111,28 +140,27 @@ module TUITD
|
|
|
111
140
|
def wait_for_stable(stable_ms: 300)
|
|
112
141
|
deadline = monotonic + @timeout
|
|
113
142
|
last_change = monotonic
|
|
114
|
-
|
|
143
|
+
last_buffer_size = @output_mutex.synchronize { @output_buffer.bytesize }
|
|
144
|
+
loop_count = 0
|
|
115
145
|
|
|
116
146
|
loop do
|
|
117
147
|
raise TimeoutError, "Timeout waiting for stable output" if monotonic > deadline
|
|
118
148
|
|
|
119
|
-
|
|
149
|
+
read_available!
|
|
150
|
+
current_buffer_size = @output_mutex.synchronize { @output_buffer.bytesize }
|
|
120
151
|
process_alive = process_alive?
|
|
121
152
|
|
|
122
|
-
if
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
last_grid = current_grid
|
|
126
|
-
last_change = monotonic
|
|
127
|
-
end
|
|
153
|
+
if current_buffer_size != last_buffer_size
|
|
154
|
+
last_buffer_size = current_buffer_size
|
|
155
|
+
last_change = monotonic
|
|
128
156
|
elsif !process_alive
|
|
129
|
-
# Process exited and no more data — final state reached
|
|
130
157
|
break
|
|
131
|
-
elsif
|
|
158
|
+
elsif (monotonic - last_change) * 1000 >= stable_ms # rubocop:disable Lint/DuplicateBranch
|
|
132
159
|
break
|
|
133
160
|
end
|
|
134
161
|
|
|
135
|
-
|
|
162
|
+
adaptive_sleep(loop_count)
|
|
163
|
+
loop_count += 1
|
|
136
164
|
end
|
|
137
165
|
refresh_state!
|
|
138
166
|
end
|
|
@@ -225,6 +253,7 @@ module TUITD
|
|
|
225
253
|
def _start_reader_thread
|
|
226
254
|
@reader_running = true
|
|
227
255
|
@reader_thread = Thread.new do
|
|
256
|
+
loop_count = 0
|
|
228
257
|
loop do
|
|
229
258
|
break unless @reader_running
|
|
230
259
|
|
|
@@ -233,7 +262,8 @@ module TUITD
|
|
|
233
262
|
rescue IOError, Errno::EIO
|
|
234
263
|
break
|
|
235
264
|
end
|
|
236
|
-
|
|
265
|
+
adaptive_sleep(loop_count)
|
|
266
|
+
loop_count += 1
|
|
237
267
|
end
|
|
238
268
|
end
|
|
239
269
|
end
|
|
@@ -260,12 +290,30 @@ module TUITD
|
|
|
260
290
|
raise Error, "Process exited (status: #{@wait_thr&.value&.exitstatus})" unless @wait_thr&.alive?
|
|
261
291
|
end
|
|
262
292
|
|
|
293
|
+
def adaptive_sleep(loop_count)
|
|
294
|
+
interval = if @poll_interval
|
|
295
|
+
@poll_interval
|
|
296
|
+
elsif loop_count < 40 # ~0-2s: 10ms
|
|
297
|
+
0.01
|
|
298
|
+
elsif loop_count < 160 # ~2-5s: 25ms
|
|
299
|
+
0.025
|
|
300
|
+
elsif loop_count < 260 # ~5-10s: 50ms
|
|
301
|
+
0.05
|
|
302
|
+
else # 10s+: 100ms
|
|
303
|
+
0.1
|
|
304
|
+
end
|
|
305
|
+
sleep interval
|
|
306
|
+
end
|
|
307
|
+
|
|
263
308
|
def read_available!
|
|
264
309
|
return false unless @stdout
|
|
265
310
|
|
|
266
311
|
data = @stdout.read_nonblock(4096)
|
|
267
312
|
|
|
268
|
-
@output_mutex.synchronize
|
|
313
|
+
@output_mutex.synchronize do
|
|
314
|
+
@output_buffer << data
|
|
315
|
+
@output_buffer = @output_buffer[-MAX_BUFFER_SIZE..] if @output_buffer.bytesize > MAX_BUFFER_SIZE
|
|
316
|
+
end
|
|
269
317
|
|
|
270
318
|
respond_to_dsr if data.include?("\e[6n")
|
|
271
319
|
|
data/lib/tui_td/matchers.rb
CHANGED
|
@@ -2,36 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
require "rspec/expectations"
|
|
4
4
|
|
|
5
|
-
# RSpec matchers for TUITD::State objects.
|
|
5
|
+
# RSpec matchers for TUITD::State and TUITD::Driver objects.
|
|
6
|
+
#
|
|
7
|
+
# When given a State, matchers check immediately.
|
|
8
|
+
# When given a Driver, matchers auto-wait (up to 3 seconds) for the condition.
|
|
6
9
|
#
|
|
7
10
|
# Usage:
|
|
8
11
|
# require "tui_td/matchers"
|
|
9
12
|
#
|
|
13
|
+
# # Immediate check on State
|
|
10
14
|
# state = TUITD::State.new(driver.state_data)
|
|
11
15
|
# expect(state).to have_text("Welcome")
|
|
12
|
-
#
|
|
16
|
+
#
|
|
17
|
+
# # Auto-wait on Driver
|
|
18
|
+
# expect(driver).to have_text("Welcome")
|
|
13
19
|
#
|
|
14
20
|
module TUITD
|
|
15
21
|
module Matchers
|
|
22
|
+
AUTO_WAIT_TIMEOUT = 3
|
|
23
|
+
|
|
24
|
+
def self.auto_wait(actual, timeout: AUTO_WAIT_TIMEOUT, &predicate)
|
|
25
|
+
if actual.respond_to?(:wait_for)
|
|
26
|
+
begin
|
|
27
|
+
actual.wait_for(timeout: timeout, &predicate)
|
|
28
|
+
true
|
|
29
|
+
rescue TUITD::TimeoutError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
predicate.call(actual)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
16
37
|
RSpec::Matchers.define :have_text do |expected|
|
|
17
|
-
match do |
|
|
18
|
-
|
|
38
|
+
match do |actual|
|
|
39
|
+
Matchers.auto_wait(actual) { |s| s.find_text(expected).any? }
|
|
19
40
|
end
|
|
20
41
|
|
|
21
42
|
description { "have text #{expected.inspect}" }
|
|
22
|
-
failure_message { |
|
|
23
|
-
failure_message_when_negated { |
|
|
43
|
+
failure_message { |_actual| "expected terminal to contain #{expected.inspect}" }
|
|
44
|
+
failure_message_when_negated { |_actual| "expected terminal NOT to contain #{expected.inspect}" }
|
|
24
45
|
end
|
|
25
46
|
|
|
26
47
|
RSpec::Matchers.define :have_regex do |pattern|
|
|
27
|
-
match do |
|
|
48
|
+
match do |actual|
|
|
28
49
|
@regex = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern.to_s)
|
|
29
|
-
|
|
50
|
+
Matchers.auto_wait(actual) { |s| s.find_text(@regex).any? }
|
|
30
51
|
end
|
|
31
52
|
|
|
32
53
|
description { "match regex #{pattern.inspect}" }
|
|
33
|
-
failure_message { |
|
|
34
|
-
failure_message_when_negated { |
|
|
54
|
+
failure_message { |_actual| "expected terminal to match #{pattern.inspect}" }
|
|
55
|
+
failure_message_when_negated { |_actual| "expected terminal NOT to match #{pattern.inspect}" }
|
|
35
56
|
end
|
|
36
57
|
|
|
37
58
|
RSpec::Matchers.define :have_fg do |expected|
|
|
@@ -40,13 +61,15 @@ module TUITD
|
|
|
40
61
|
@col = col
|
|
41
62
|
end
|
|
42
63
|
|
|
43
|
-
match do |
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
match do |actual|
|
|
65
|
+
Matchers.auto_wait(actual) do |s|
|
|
66
|
+
@actual = s.foreground_at(@row, @col)
|
|
67
|
+
@actual == expected
|
|
68
|
+
end
|
|
46
69
|
end
|
|
47
70
|
|
|
48
71
|
description { "have foreground #{expected.inspect} at [#{@row},#{@col}]" }
|
|
49
|
-
failure_message do |
|
|
72
|
+
failure_message do |_actual|
|
|
50
73
|
"expected FG at [#{@row},#{@col}] to be #{expected.inspect}, but was #{@actual.inspect}"
|
|
51
74
|
end
|
|
52
75
|
end
|
|
@@ -57,13 +80,15 @@ module TUITD
|
|
|
57
80
|
@col = col
|
|
58
81
|
end
|
|
59
82
|
|
|
60
|
-
match do |
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
match do |actual|
|
|
84
|
+
Matchers.auto_wait(actual) do |s|
|
|
85
|
+
@actual = s.background_at(@row, @col)
|
|
86
|
+
@actual == expected
|
|
87
|
+
end
|
|
63
88
|
end
|
|
64
89
|
|
|
65
90
|
description { "have background #{expected.inspect} at [#{@row},#{@col}]" }
|
|
66
|
-
failure_message do |
|
|
91
|
+
failure_message do |_actual|
|
|
67
92
|
"expected BG at [#{@row},#{@col}] to be #{expected.inspect}, but was #{@actual.inspect}"
|
|
68
93
|
end
|
|
69
94
|
end
|
|
@@ -75,17 +100,19 @@ module TUITD
|
|
|
75
100
|
end
|
|
76
101
|
chain(:with) { |expected| @expected = expected }
|
|
77
102
|
|
|
78
|
-
match do |
|
|
79
|
-
@
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
match do |actual|
|
|
104
|
+
@expected_style = @expected || {}
|
|
105
|
+
Matchers.auto_wait(actual) do |s|
|
|
106
|
+
@actual = s.style_at(@row, @col)
|
|
107
|
+
@expected_style.all? { |k, v| @actual[k] == v }
|
|
108
|
+
end
|
|
82
109
|
end
|
|
83
110
|
|
|
84
111
|
description do
|
|
85
|
-
"have style #{@
|
|
112
|
+
"have style #{@expected_style.inspect} at [#{@row},#{@col}]"
|
|
86
113
|
end
|
|
87
|
-
failure_message do |
|
|
88
|
-
"expected style at [#{@row},#{@col}] to be #{@
|
|
114
|
+
failure_message do |_actual|
|
|
115
|
+
"expected style at [#{@row},#{@col}] to be #{@expected_style.inspect}, but was #{@actual.inspect}"
|
|
89
116
|
end
|
|
90
117
|
end
|
|
91
118
|
|
|
@@ -104,5 +131,84 @@ module TUITD
|
|
|
104
131
|
"expected exit status not to be #{expected}"
|
|
105
132
|
end
|
|
106
133
|
end
|
|
134
|
+
|
|
135
|
+
# Selector-based matchers — work with both State and Driver (auto-wait)
|
|
136
|
+
|
|
137
|
+
RSpec::Matchers.define :have_button do |expected|
|
|
138
|
+
match do |actual|
|
|
139
|
+
Matchers.auto_wait(actual) do |s|
|
|
140
|
+
Selector.new(s).get_by_text(expected).any? { |e| e.role == :button }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
description { "have button #{expected.inspect}" }
|
|
145
|
+
failure_message { |_actual| "expected terminal to have a button #{expected.inspect}" }
|
|
146
|
+
failure_message_when_negated { |_actual| "expected terminal NOT to have a button #{expected.inspect}" }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
RSpec::Matchers.define :have_dialog do
|
|
150
|
+
match do |actual|
|
|
151
|
+
Matchers.auto_wait(actual) { |s| Selector.new(s).dialogs.any? }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
description { "have a dialog" }
|
|
155
|
+
failure_message { |_actual| "expected terminal to have a dialog" }
|
|
156
|
+
failure_message_when_negated { |_actual| "expected terminal NOT to have a dialog" }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
RSpec::Matchers.define :have_checkbox do |expected|
|
|
160
|
+
chain(:checked) { @checked = true }
|
|
161
|
+
|
|
162
|
+
match do |actual|
|
|
163
|
+
Matchers.auto_wait(actual) do |s|
|
|
164
|
+
checkboxes = Selector.new(s).checkboxes
|
|
165
|
+
found = checkboxes.select { |e| e.text&.include?(expected) }
|
|
166
|
+
found = found.select(&:checked) if @checked
|
|
167
|
+
found.any?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
description do
|
|
172
|
+
desc = "have checkbox #{expected.inspect}"
|
|
173
|
+
desc += " (checked)" if @checked
|
|
174
|
+
desc
|
|
175
|
+
end
|
|
176
|
+
failure_message do |_actual|
|
|
177
|
+
desc = "expected terminal to have checkbox #{expected.inspect}"
|
|
178
|
+
desc += " (checked)" if @checked
|
|
179
|
+
desc
|
|
180
|
+
end
|
|
181
|
+
failure_message_when_negated do |_actual|
|
|
182
|
+
desc = "expected terminal NOT to have checkbox #{expected.inspect}"
|
|
183
|
+
desc += " (checked)" if @checked
|
|
184
|
+
desc
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
RSpec::Matchers.define :have_role do |role, text: nil|
|
|
189
|
+
match do |actual|
|
|
190
|
+
Matchers.auto_wait(actual) do |s|
|
|
191
|
+
elements = Selector.new(s).get_by_role(role)
|
|
192
|
+
elements = elements.select { |e| e.text&.include?(text) } if text
|
|
193
|
+
elements.any?
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
description do
|
|
198
|
+
desc = "have role :#{role}"
|
|
199
|
+
desc += " with text #{text.inspect}" if text
|
|
200
|
+
desc
|
|
201
|
+
end
|
|
202
|
+
failure_message do |_actual|
|
|
203
|
+
desc = "expected terminal to have a :#{role}"
|
|
204
|
+
desc += " with text #{text.inspect}" if text
|
|
205
|
+
desc
|
|
206
|
+
end
|
|
207
|
+
failure_message_when_negated do |_actual|
|
|
208
|
+
desc = "expected terminal NOT to have a :#{role}"
|
|
209
|
+
desc += " with text #{text.inspect}" if text
|
|
210
|
+
desc
|
|
211
|
+
end
|
|
212
|
+
end
|
|
107
213
|
end
|
|
108
214
|
end
|
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
|
|
|
@@ -284,6 +284,23 @@ module TUITD
|
|
|
284
284
|
required: ["pattern"],
|
|
285
285
|
},
|
|
286
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
|
+
},
|
|
287
304
|
{
|
|
288
305
|
name: "tui_close",
|
|
289
306
|
description: "Close the TUI application and clean up the PTY session. Call this when finished.",
|
|
@@ -315,7 +332,8 @@ module TUITD
|
|
|
315
332
|
when "tui_wait_for_exit" then call_tui_wait_for_exit
|
|
316
333
|
when "tui_exit_status" then call_tui_exit_status
|
|
317
334
|
when "tui_find_text" then call_tui_find_text(args)
|
|
318
|
-
when "
|
|
335
|
+
when "tui_find_elements" then call_tui_find_elements(args)
|
|
336
|
+
when "tui_close" then call_tui_close
|
|
319
337
|
else
|
|
320
338
|
return error_response(id, -32_602, "Unknown tool: #{tool_name}")
|
|
321
339
|
end
|
|
@@ -488,6 +506,39 @@ module TUITD
|
|
|
488
506
|
end
|
|
489
507
|
end
|
|
490
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
|
+
|
|
491
542
|
def call_tui_close
|
|
492
543
|
@driver&.close
|
|
493
544
|
@driver = nil
|
|
@@ -544,4 +595,4 @@ module TUITD
|
|
|
544
595
|
end
|
|
545
596
|
end
|
|
546
597
|
end
|
|
547
|
-
# 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/test_runner.rb
CHANGED
|
@@ -109,18 +109,30 @@ module TUITD
|
|
|
109
109
|
when "assert_style"
|
|
110
110
|
ensure_driver!(driver)
|
|
111
111
|
row, col = coords(step)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
119
130
|
if match
|
|
120
|
-
Result.new(step: action, passed: true,
|
|
131
|
+
Result.new(step: action, passed: true,
|
|
132
|
+
message: "Style at [#{row},#{col}] matches #{expected_style}",)
|
|
121
133
|
else
|
|
122
134
|
Result.new(step: action, passed: false,
|
|
123
|
-
message: "Style at [#{row},#{col}] is #{actual}, expected #{
|
|
135
|
+
message: "Style at [#{row},#{col}] is #{actual}, expected #{expected_style}",)
|
|
124
136
|
end
|
|
125
137
|
|
|
126
138
|
when "screenshot"
|
|
@@ -151,6 +163,19 @@ module TUITD
|
|
|
151
163
|
Result.new(step: action, passed: false, message: "Exit status #{actual}, expected #{expected}")
|
|
152
164
|
end
|
|
153
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
|
+
|
|
154
179
|
when "close"
|
|
155
180
|
driver&.close
|
|
156
181
|
driver = nil
|
|
@@ -199,6 +224,25 @@ module TUITD
|
|
|
199
224
|
|
|
200
225
|
private
|
|
201
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
|
+
|
|
202
246
|
def safe_output_path(value, ext)
|
|
203
247
|
default = File.join("/tmp", "tui_td_#{Time.now.to_i}.#{ext}")
|
|
204
248
|
resolved = File.expand_path(value.is_a?(String) ? value : default)
|
|
@@ -223,7 +267,6 @@ module TUITD
|
|
|
223
267
|
|
|
224
268
|
def check_text(driver, value, action)
|
|
225
269
|
ensure_driver!(driver)
|
|
226
|
-
state = State.new(driver.state_data)
|
|
227
270
|
text = value.to_s
|
|
228
271
|
|
|
229
272
|
if action == "assert_regex"
|
|
@@ -236,7 +279,17 @@ module TUITD
|
|
|
236
279
|
pattern = text
|
|
237
280
|
end
|
|
238
281
|
|
|
239
|
-
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
|
|
240
293
|
|
|
241
294
|
case action
|
|
242
295
|
when "assert_text"
|
|
@@ -266,15 +319,30 @@ module TUITD
|
|
|
266
319
|
ensure_driver!(driver)
|
|
267
320
|
row, col = coords(step)
|
|
268
321
|
expected = step[:is] || step["is"]
|
|
269
|
-
state = State.new(driver.state_data)
|
|
270
322
|
label = property == :fg ? "FG" : "BG"
|
|
271
|
-
actual = property == :fg ? state.foreground_at(row, col) : state.background_at(row, col)
|
|
272
323
|
|
|
273
|
-
|
|
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
|
|
274
342
|
Result.new(step: step.keys.first.to_s, passed: true, message: "#{label} at [#{row},#{col}] is #{expected}")
|
|
275
343
|
else
|
|
276
344
|
Result.new(step: step.keys.first.to_s, passed: false,
|
|
277
|
-
message: "#{label} at [#{row},#{col}] is #{
|
|
345
|
+
message: "#{label} at [#{row},#{col}] is #{actual_val}, expected #{expected}",)
|
|
278
346
|
end
|
|
279
347
|
end
|
|
280
348
|
end
|
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.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haluk Durmus
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version:
|
|
74
|
+
version: 0.1.1
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version:
|
|
81
|
+
version: 0.1.1
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: bundler-audit
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -173,6 +173,7 @@ files:
|
|
|
173
173
|
- lib/tui_td/matchers.rb
|
|
174
174
|
- lib/tui_td/mcp/server.rb
|
|
175
175
|
- lib/tui_td/screenshot.rb
|
|
176
|
+
- lib/tui_td/selector.rb
|
|
176
177
|
- lib/tui_td/state.rb
|
|
177
178
|
- lib/tui_td/test_runner.rb
|
|
178
179
|
- lib/tui_td/unifont_glyphs.rb
|