tui-td 0.2.4 → 0.2.5
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 +6 -0
- data/README.md +26 -52
- data/lib/tui_td/ansi_parser.rb +73 -29
- data/lib/tui_td/ansi_utils.rb +75 -0
- data/lib/tui_td/html_renderer.rb +3 -66
- data/lib/tui_td/screenshot.rb +3 -70
- data/lib/tui_td/state.rb +3 -0
- data/lib/tui_td/test_runner.rb +141 -123
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5a923ea75814849ee5cf0633c81c09e2db156e47df52f6f92f2b314f743f3b7
|
|
4
|
+
data.tar.gz: c5cb55618e38bcc06a477bedce42d61d8f0c497ff38f887462943e8d094863b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d36aac819998194c942dc9fc16c96ef0b4ef9fa7d549649f4bdd78cd59687858939ef7e1303d215a13e1dcd0957350d3839e43af94d4b837eb509ea5541cb58
|
|
7
|
+
data.tar.gz: 3d9f2fe282a9e32a274cea860ef5592e7666ce48121d590395c926bd0119d0f970ad2d2f232b88b0323742df8ef42b18686cd60ca75007d9f739cc2e8f4bae5b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
- MCP smoke test expanded: 20 → 54 assertions, covers all 10 tools plus error paths (88% server coverage)
|
|
6
|
+
- Extracted `ansi_utils.rb` — shared ANSI helpers used by parser, renderer, and screenshot
|
|
7
|
+
- New RSpec specs for `ansi_utils`, enhanced specs for `test_runner`, `html_renderer`, `matchers`, `state`, `ansi_parser`
|
|
8
|
+
|
|
3
9
|
## 0.2.4
|
|
4
10
|
|
|
5
11
|
- `assert_regex` JSON test step — match terminal output against a Ruby regex
|
data/README.md
CHANGED
|
@@ -1,57 +1,23 @@
|
|
|
1
1
|
# TUI Test Drive
|
|
2
2
|
|
|
3
|
-
*Like Playwright or Puppeteer, but built specifically for Terminal UIs and optimized for AI Coding Agents.*
|
|
4
|
-
|
|
5
3
|
Testing framework for Terminal User Interfaces (TUIs) with MCP support.
|
|
6
4
|
|
|
7
|
-
A Ruby library, but language-agnostic through its JSON test format and MCP server — use it from Python, JavaScript, Go, or any other programming language on Linux and macOS.
|
|
8
|
-
|
|
9
5
|
**tui-td** lets you:
|
|
10
6
|
1. Start a TUI application in a virtual terminal (PTY)
|
|
11
7
|
2. See the output — as structured JSON, plain text, PNG screenshots, or HTML renders
|
|
12
8
|
3. Send input — keystrokes, text, control sequences
|
|
13
9
|
4. Analyze output — find text, check colors, detect cursor position
|
|
14
10
|
5. Loop — adjust and retest without manual intervention
|
|
15
|
-
6. Integrate — works with any language via JSON test files or MCP
|
|
16
11
|
|
|
17
12
|
## Installation
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
**rbenv (recommended):**
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# macOS
|
|
25
|
-
brew install rbenv ruby-build
|
|
26
|
-
echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
|
|
27
|
-
|
|
28
|
-
# Linux
|
|
29
|
-
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
|
|
30
|
-
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
|
31
|
-
echo 'eval "$(~/.rbenv/bin/rbenv init - bash)"' >> ~/.bashrc
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Then install Ruby 3 and activate it:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
rbenv install 3.4.1
|
|
38
|
-
rbenv global 3.4.1
|
|
39
|
-
ruby --version # must show 3.0+
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Alternative — Homebrew (macOS):**
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
brew install ruby
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### 2. Install tui-td
|
|
14
|
+
Ruby 3.0+ is required. Install via [rbenv](https://github.com/rbenv/rbenv#installation) or `brew install ruby`.
|
|
49
15
|
|
|
50
16
|
```bash
|
|
51
17
|
gem install tui-td
|
|
52
18
|
```
|
|
53
19
|
|
|
54
|
-
|
|
20
|
+
Quick test:
|
|
55
21
|
|
|
56
22
|
```bash
|
|
57
23
|
tui-td capture "echo Hello World"
|
|
@@ -124,20 +90,20 @@ Interactive commands (drive mode):
|
|
|
124
90
|
exit Quit drive mode
|
|
125
91
|
|
|
126
92
|
Global options:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
93
|
+
-r, --rows N Terminal rows (default: 40)
|
|
94
|
+
-c, --cols N Terminal cols (default: 120)
|
|
95
|
+
-t, --timeout SECONDS Timeout in seconds (default: 30)
|
|
96
|
+
-C, --chdir PATH Working directory for the command
|
|
97
|
+
--screenshot PATH Save screenshot (e.g., output.png)
|
|
98
|
+
--html PATH Save HTML render (e.g., output.html)
|
|
99
|
+
--json Output state as compact JSON
|
|
100
|
+
--pretty Output state as pretty JSON
|
|
101
|
+
--text Output state as plain text table
|
|
102
|
+
-v, --verbose Show each test step as it runs
|
|
103
|
+
-l, --live Show terminal state after each step (screen-refresh)
|
|
104
|
+
-s, --step Pause after each test step for confirmation
|
|
105
|
+
--version Show version
|
|
106
|
+
-h, --help Show help
|
|
141
107
|
```
|
|
142
108
|
|
|
143
109
|
`tui-td --help` serves as the full CLI reference. `tui-td help test` shows all JSON test
|
|
@@ -271,12 +237,18 @@ tui-td test examples/echo_test.json
|
|
|
271
237
|
"rows": 24,
|
|
272
238
|
"cols": 80,
|
|
273
239
|
"timeout": 10,
|
|
240
|
+
"chdir": "/path/to/project",
|
|
241
|
+
"before_all": [
|
|
242
|
+
{ "start": "my_tui_app", "env": { "DATABASE_URL": "test://" } }
|
|
243
|
+
],
|
|
274
244
|
"steps": [
|
|
275
|
-
{ "start": "my_tui_app" },
|
|
276
245
|
{ "wait_for_text": "> " },
|
|
277
246
|
{ "send": "hello\n" },
|
|
278
247
|
{ "assert_text": "hello" },
|
|
279
|
-
{ "
|
|
248
|
+
{ "assert_regex": "hello|world" },
|
|
249
|
+
{ "assert_fg": [0, 0], "is": "cyan" }
|
|
250
|
+
],
|
|
251
|
+
"after_all": [
|
|
280
252
|
{ "close": true }
|
|
281
253
|
]
|
|
282
254
|
}
|
|
@@ -371,9 +343,11 @@ end
|
|
|
371
343
|
| Matcher | Usage |
|
|
372
344
|
|---------|-------|
|
|
373
345
|
| `have_text("...")` | Assert text is present on screen |
|
|
346
|
+
| `have_regex(/pattern/)` | Assert regex pattern matches anywhere |
|
|
374
347
|
| `have_fg("color").at(row, col)` | Assert foreground color at position |
|
|
375
348
|
| `have_bg("color").at(row, col)` | Assert background color at position |
|
|
376
349
|
| `have_style.at(row, col).with(bold: true, ...)` | Assert cell style |
|
|
350
|
+
| `have_exit_status(N)` | Assert the driver process exit status equals N |
|
|
377
351
|
|
|
378
352
|
## MCP Server — AI Integration
|
|
379
353
|
|
data/lib/tui_td/ansi_parser.rb
CHANGED
|
@@ -83,15 +83,13 @@ module TUITD
|
|
|
83
83
|
# Parse raw terminal output into a structured state Hash
|
|
84
84
|
def self.parse(raw, rows = 40, cols = 120)
|
|
85
85
|
grid = Array.new(rows) do
|
|
86
|
-
Array.new(cols)
|
|
87
|
-
{ char: " ", fg: "default", bg: "default", bold: false, italic: false, underline: false }
|
|
88
|
-
end
|
|
86
|
+
Array.new(cols) { default_cell.dup }
|
|
89
87
|
end
|
|
90
88
|
|
|
91
89
|
cursor = { row: 0, col: 0 }
|
|
92
90
|
attrs = { fg: "default", bg: "default", bold: false, italic: false, underline: false }
|
|
93
91
|
saved_cursor = nil
|
|
94
|
-
scroll_region =
|
|
92
|
+
scroll_region = { top: 0, bottom: rows - 1 }
|
|
95
93
|
pending_dsr = false
|
|
96
94
|
|
|
97
95
|
# Strip everything before the last full clear (if any)
|
|
@@ -103,11 +101,12 @@ module TUITD
|
|
|
103
101
|
if processed[i] == "\e" && processed[i + 1] == "["
|
|
104
102
|
# Find end of CSI sequence
|
|
105
103
|
j = i + 2
|
|
106
|
-
j += 1 while j < processed.length && !processed[j].match?(/[A-HJ-KP-SX@`
|
|
104
|
+
j += 1 while j < processed.length && !processed[j].match?(/[A-HJ-KP-SX@`fhlmnRrsu]/)
|
|
107
105
|
seq = processed[i..j]
|
|
108
106
|
|
|
109
|
-
dsr = _apply_csi(seq, cursor, attrs, grid, rows, cols)
|
|
107
|
+
dsr, new_saved = _apply_csi(seq, cursor, attrs, grid, rows, cols, saved_cursor, scroll_region)
|
|
110
108
|
pending_dsr ||= dsr
|
|
109
|
+
saved_cursor = new_saved if new_saved
|
|
111
110
|
|
|
112
111
|
i = j + 1
|
|
113
112
|
elsif processed[i] == "\n" || processed[i] == "\r\n"
|
|
@@ -128,11 +127,20 @@ module TUITD
|
|
|
128
127
|
# Bell — ignore
|
|
129
128
|
i += 1
|
|
130
129
|
elsif processed[i] == "\e"
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
# Handle non-CSI escape sequences
|
|
131
|
+
if processed[i + 1] == "7"
|
|
132
|
+
# DECSC — Save Cursor
|
|
133
|
+
saved_cursor = { row: cursor[:row], col: cursor[:col] }
|
|
134
|
+
i += 2
|
|
135
|
+
elsif processed[i + 1] == "8"
|
|
136
|
+
# DECRC — Restore Cursor
|
|
137
|
+
if saved_cursor
|
|
138
|
+
cursor[:row] = saved_cursor[:row]
|
|
139
|
+
cursor[:col] = saved_cursor[:col]
|
|
140
|
+
end
|
|
141
|
+
i += 2
|
|
142
|
+
elsif processed[i + 1] && processed[i + 1].match?(/[()*+\-.\/]/)
|
|
143
|
+
# ISO 2022 charset: \e( B \e) 0 etc. (3 chars total)
|
|
136
144
|
i += 3
|
|
137
145
|
else
|
|
138
146
|
i += 1
|
|
@@ -151,14 +159,22 @@ module TUITD
|
|
|
151
159
|
i += 1
|
|
152
160
|
end
|
|
153
161
|
|
|
154
|
-
# Handle scrolling
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
# Handle scrolling within the defined scroll region
|
|
163
|
+
region_top = scroll_region[:top]
|
|
164
|
+
region_bottom = scroll_region[:bottom]
|
|
165
|
+
|
|
166
|
+
if cursor[:row] > region_bottom
|
|
167
|
+
scroll_lines = [cursor[:row] - region_bottom, rows].min
|
|
168
|
+
# Shift lines within the scroll region up
|
|
169
|
+
(region_top..(region_bottom - scroll_lines)).each do |ri|
|
|
170
|
+
src = ri + scroll_lines
|
|
171
|
+
grid[ri] = src <= region_bottom ? grid[src] : Array.new(cols) { default_cell.dup }
|
|
172
|
+
end
|
|
173
|
+
# Fill bottom of scroll region with blank lines
|
|
174
|
+
((region_bottom - scroll_lines + 1)..region_bottom).each do |ri|
|
|
175
|
+
grid[ri] = Array.new(cols) { default_cell.dup }
|
|
160
176
|
end
|
|
161
|
-
cursor[:row] =
|
|
177
|
+
cursor[:row] = region_bottom
|
|
162
178
|
end
|
|
163
179
|
end
|
|
164
180
|
|
|
@@ -211,15 +227,17 @@ module TUITD
|
|
|
211
227
|
out
|
|
212
228
|
end
|
|
213
229
|
|
|
214
|
-
def self._apply_csi(seq, cursor, attrs, grid, rows, cols)
|
|
230
|
+
def self._apply_csi(seq, cursor, attrs, grid, rows, cols, saved_cursor, scroll_region)
|
|
215
231
|
# Strip leading escape char if present
|
|
216
232
|
cleaned = seq.sub(/^\e/, "")
|
|
217
|
-
match = cleaned.match(/^\[([\d;]*)([A-HJ-KP-SX@`
|
|
218
|
-
return unless match
|
|
233
|
+
match = cleaned.match(/^\[([\d;]*)([A-HJ-KP-SX@`fhlmnRrsu])$/)
|
|
234
|
+
return [false, nil] unless match
|
|
219
235
|
|
|
220
236
|
params = match[1].split(";").map(&:to_i)
|
|
221
237
|
command = match[2]
|
|
222
238
|
|
|
239
|
+
new_saved = nil
|
|
240
|
+
|
|
223
241
|
case command
|
|
224
242
|
when "m"
|
|
225
243
|
_apply_sgr(params, attrs)
|
|
@@ -270,14 +288,36 @@ module TUITD
|
|
|
270
288
|
next unless cursor[:row] < rows && cursor[:col] + i < cols
|
|
271
289
|
grid[cursor[:row]][cursor[:col] + i][:char] = " "
|
|
272
290
|
end
|
|
273
|
-
when "
|
|
291
|
+
when "s" # DECSC — Save Cursor (CSI variant)
|
|
292
|
+
new_saved = { row: cursor[:row], col: cursor[:col] }
|
|
293
|
+
when "u" # DECRC — Restore Cursor (CSI variant)
|
|
294
|
+
if saved_cursor
|
|
295
|
+
cursor[:row] = saved_cursor[:row]
|
|
296
|
+
cursor[:col] = saved_cursor[:col]
|
|
297
|
+
end
|
|
298
|
+
when "r" # DECSTBM — Set Scroll Region
|
|
299
|
+
top = (params[0] || 1) - 1
|
|
300
|
+
bottom = (params[1] || rows) - 1
|
|
301
|
+
top = top.clamp(0, rows - 1)
|
|
302
|
+
bottom = bottom.clamp(0, rows - 1)
|
|
303
|
+
if top < bottom
|
|
304
|
+
scroll_region[:top] = top
|
|
305
|
+
scroll_region[:bottom] = bottom
|
|
306
|
+
else
|
|
307
|
+
scroll_region[:top] = 0
|
|
308
|
+
scroll_region[:bottom] = rows - 1
|
|
309
|
+
end
|
|
310
|
+
cursor[:row] = 0
|
|
311
|
+
cursor[:col] = 0
|
|
312
|
+
when "h", "l" # DEC private mode set/reset — skip
|
|
274
313
|
nil
|
|
275
314
|
when "n" # DSR — Device Status Report request
|
|
276
|
-
|
|
277
|
-
return params[0] == 6
|
|
315
|
+
return [params[0] == 6, nil]
|
|
278
316
|
when "R" # DSR response (from terminal side) or CPR — ignore
|
|
279
317
|
nil
|
|
280
318
|
end
|
|
319
|
+
|
|
320
|
+
[false, new_saved]
|
|
281
321
|
end
|
|
282
322
|
|
|
283
323
|
def self._apply_sgr(params, attrs)
|
|
@@ -397,21 +437,25 @@ module TUITD
|
|
|
397
437
|
def self._color_code(name, prefix)
|
|
398
438
|
case name
|
|
399
439
|
when "default" then nil
|
|
400
|
-
when /^(bright_)?(.+)$/
|
|
401
|
-
base_name = $2
|
|
402
|
-
index = SGR_16_TO_NAME.key(base_name)
|
|
403
|
-
index += 8 if $1 && index && index < 8
|
|
404
|
-
index ? "#{prefix};5;#{index}" : nil
|
|
405
440
|
when /^#([0-9a-fA-F]{6})$/
|
|
406
441
|
r = $1[0..1].to_i(16)
|
|
407
442
|
g = $1[2..3].to_i(16)
|
|
408
443
|
b = $1[4..5].to_i(16)
|
|
409
444
|
"#{prefix};2;#{r};#{g};#{b}"
|
|
445
|
+
when /^(bright_)?(.+)$/
|
|
446
|
+
base_name = $2
|
|
447
|
+
index = SGR_16_TO_NAME.key(base_name)
|
|
448
|
+
index += 8 if $1 && index && index < 8
|
|
449
|
+
index ? "#{prefix};5;#{index}" : nil
|
|
410
450
|
else
|
|
411
451
|
nil
|
|
412
452
|
end
|
|
413
453
|
end
|
|
414
454
|
|
|
455
|
+
def self.default_cell
|
|
456
|
+
{ char: " ", fg: "default", bg: "default", bold: false, italic: false, underline: false }
|
|
457
|
+
end
|
|
458
|
+
|
|
415
459
|
# Extract a single UTF-8 character at position i in a binary string.
|
|
416
460
|
# Returns [char_string, byte_length] or nil if the byte is not printable/valid.
|
|
417
461
|
def self._utf8_char_at(str, i)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TUITD
|
|
4
|
+
# Shared ANSI color constants and helpers.
|
|
5
|
+
# Used by Screenshot, HtmlRenderer, and other color-aware renderers.
|
|
6
|
+
module ANSIUtils
|
|
7
|
+
ANSI_RGB = {
|
|
8
|
+
"black" => [0x00, 0x00, 0x00],
|
|
9
|
+
"red" => [0xAA, 0x00, 0x00],
|
|
10
|
+
"green" => [0x00, 0xAA, 0x00],
|
|
11
|
+
"yellow" => [0xAA, 0x55, 0x00],
|
|
12
|
+
"blue" => [0x00, 0x00, 0xAA],
|
|
13
|
+
"magenta" => [0xAA, 0x00, 0xAA],
|
|
14
|
+
"cyan" => [0x00, 0xAA, 0xAA],
|
|
15
|
+
"white" => [0xAA, 0xAA, 0xAA],
|
|
16
|
+
"bright_black" => [0x55, 0x55, 0x55],
|
|
17
|
+
"bright_red" => [0xFF, 0x55, 0x55],
|
|
18
|
+
"bright_green" => [0x55, 0xFF, 0x55],
|
|
19
|
+
"bright_yellow" => [0xFF, 0xFF, 0x55],
|
|
20
|
+
"bright_blue" => [0x55, 0x55, 0xFF],
|
|
21
|
+
"bright_magenta"=> [0xFF, 0x55, 0xFF],
|
|
22
|
+
"bright_cyan" => [0x55, 0xFF, 0xFF],
|
|
23
|
+
"bright_white" => [0xFF, 0xFF, 0xFF],
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
CUBE = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF].freeze
|
|
27
|
+
|
|
28
|
+
ANSI_INDEX = %w[
|
|
29
|
+
black red green yellow blue magenta cyan white
|
|
30
|
+
bright_black bright_red bright_green bright_yellow
|
|
31
|
+
bright_blue bright_magenta bright_cyan bright_white
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
DEFAULT_FG = [0xC0, 0xC0, 0xC0].freeze
|
|
35
|
+
DEFAULT_BG = [0x00, 0x00, 0x00].freeze
|
|
36
|
+
|
|
37
|
+
def resolve_color(name, fallback)
|
|
38
|
+
case name
|
|
39
|
+
when "default"
|
|
40
|
+
fallback
|
|
41
|
+
when /^#([0-9a-fA-F]{6})$/
|
|
42
|
+
[$1[0..1].to_i(16), $1[2..3].to_i(16), $1[4..5].to_i(16)]
|
|
43
|
+
when /\Acolor(\d+)\z/
|
|
44
|
+
xterm_256($1.to_i)
|
|
45
|
+
when /\Abright_(.+)\z/
|
|
46
|
+
ANSI_RGB[name] || fallback
|
|
47
|
+
else
|
|
48
|
+
ANSI_RGB[name] || fallback
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def xterm_256(index)
|
|
53
|
+
if index < 16
|
|
54
|
+
name = ANSI_INDEX[index]
|
|
55
|
+
ANSI_RGB[name] || DEFAULT_FG
|
|
56
|
+
elsif index < 232
|
|
57
|
+
r = CUBE[((index - 16) / 36) % 6]
|
|
58
|
+
g = CUBE[((index - 16) / 6) % 6]
|
|
59
|
+
b = CUBE[(index - 16) % 6]
|
|
60
|
+
[r, g, b]
|
|
61
|
+
else
|
|
62
|
+
v = 8 + (index - 232) * 10
|
|
63
|
+
[v, v, v]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def _dig(hash, *keys)
|
|
68
|
+
keys.each do |k|
|
|
69
|
+
return nil unless hash
|
|
70
|
+
hash = hash[k] || hash[k.to_s]
|
|
71
|
+
end
|
|
72
|
+
hash
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/tui_td/html_renderer.rb
CHANGED
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "ansi_utils"
|
|
4
|
+
|
|
3
5
|
module TUITD
|
|
4
6
|
# Renders terminal state as a self-contained HTML document.
|
|
5
7
|
# Faithfully reproduces what a TUI application shows — colors, styles,
|
|
6
8
|
# cursor position — so an LLM or human can "see" the terminal.
|
|
7
9
|
class HtmlRenderer
|
|
8
|
-
|
|
9
|
-
"black" => [0x00, 0x00, 0x00],
|
|
10
|
-
"red" => [0xAA, 0x00, 0x00],
|
|
11
|
-
"green" => [0x00, 0xAA, 0x00],
|
|
12
|
-
"yellow" => [0xAA, 0x55, 0x00],
|
|
13
|
-
"blue" => [0x00, 0x00, 0xAA],
|
|
14
|
-
"magenta" => [0xAA, 0x00, 0xAA],
|
|
15
|
-
"cyan" => [0x00, 0xAA, 0xAA],
|
|
16
|
-
"white" => [0xAA, 0xAA, 0xAA],
|
|
17
|
-
"bright_black" => [0x55, 0x55, 0x55],
|
|
18
|
-
"bright_red" => [0xFF, 0x55, 0x55],
|
|
19
|
-
"bright_green" => [0x55, 0xFF, 0x55],
|
|
20
|
-
"bright_yellow" => [0xFF, 0xFF, 0x55],
|
|
21
|
-
"bright_blue" => [0x55, 0x55, 0xFF],
|
|
22
|
-
"bright_magenta"=> [0xFF, 0x55, 0xFF],
|
|
23
|
-
"bright_cyan" => [0x55, 0xFF, 0xFF],
|
|
24
|
-
"bright_white" => [0xFF, 0xFF, 0xFF],
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
27
|
-
CUBE = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF].freeze
|
|
28
|
-
|
|
29
|
-
ANSI_INDEX = %w[
|
|
30
|
-
black red green yellow blue magenta cyan white
|
|
31
|
-
bright_black bright_red bright_green bright_yellow
|
|
32
|
-
bright_blue bright_magenta bright_cyan bright_white
|
|
33
|
-
].freeze
|
|
34
|
-
|
|
35
|
-
DEFAULT_FG = [0xC0, 0xC0, 0xC0].freeze
|
|
36
|
-
DEFAULT_BG = [0x00, 0x00, 0x00].freeze
|
|
10
|
+
include ANSIUtils
|
|
37
11
|
|
|
38
12
|
def initialize(state)
|
|
39
13
|
@state = state
|
|
@@ -173,36 +147,6 @@ module TUITD
|
|
|
173
147
|
@cursor[:row] == ri && @cursor[:col] == ci
|
|
174
148
|
end
|
|
175
149
|
|
|
176
|
-
def resolve_color(name, fallback)
|
|
177
|
-
case name
|
|
178
|
-
when "default"
|
|
179
|
-
fallback
|
|
180
|
-
when /^#([0-9a-fA-F]{6})$/
|
|
181
|
-
[$1[0..1].to_i(16), $1[2..3].to_i(16), $1[4..5].to_i(16)]
|
|
182
|
-
when /\Acolor(\d+)\z/
|
|
183
|
-
xterm_256($1.to_i)
|
|
184
|
-
when /\Abright_(.+)\z/
|
|
185
|
-
ANSI_RGB[name] || fallback
|
|
186
|
-
else
|
|
187
|
-
ANSI_RGB[name] || fallback
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def xterm_256(index)
|
|
192
|
-
if index < 16
|
|
193
|
-
name = ANSI_INDEX[index]
|
|
194
|
-
ANSI_RGB[name] || DEFAULT_FG
|
|
195
|
-
elsif index < 232
|
|
196
|
-
r = CUBE[((index - 16) / 36) % 6]
|
|
197
|
-
g = CUBE[((index - 16) / 6) % 6]
|
|
198
|
-
b = CUBE[(index - 16) % 6]
|
|
199
|
-
[r, g, b]
|
|
200
|
-
else
|
|
201
|
-
v = 8 + (index - 232) * 10
|
|
202
|
-
[v, v, v]
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
150
|
def css_color(rgb)
|
|
207
151
|
format("#%02x%02x%02x", *rgb)
|
|
208
152
|
end
|
|
@@ -217,12 +161,5 @@ module TUITD
|
|
|
217
161
|
end
|
|
218
162
|
end
|
|
219
163
|
|
|
220
|
-
def _dig(hash, *keys)
|
|
221
|
-
keys.each do |k|
|
|
222
|
-
return nil unless hash
|
|
223
|
-
hash = hash[k] || hash[k.to_s]
|
|
224
|
-
end
|
|
225
|
-
hash
|
|
226
|
-
end
|
|
227
164
|
end
|
|
228
165
|
end
|
data/lib/tui_td/screenshot.rb
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "chunky_png"
|
|
4
|
+
require_relative "ansi_utils"
|
|
4
5
|
|
|
5
6
|
module TUITD
|
|
6
7
|
class Screenshot
|
|
8
|
+
include ANSIUtils
|
|
9
|
+
|
|
7
10
|
CELL_W = 8
|
|
8
11
|
CELL_H = 16
|
|
9
12
|
|
|
@@ -106,39 +109,6 @@ module TUITD
|
|
|
106
109
|
].freeze
|
|
107
110
|
private_constant :FONT
|
|
108
111
|
|
|
109
|
-
ANSI_RGB = {
|
|
110
|
-
"black" => [0x00, 0x00, 0x00],
|
|
111
|
-
"red" => [0xAA, 0x00, 0x00],
|
|
112
|
-
"green" => [0x00, 0xAA, 0x00],
|
|
113
|
-
"yellow" => [0xAA, 0x55, 0x00],
|
|
114
|
-
"blue" => [0x00, 0x00, 0xAA],
|
|
115
|
-
"magenta" => [0xAA, 0x00, 0xAA],
|
|
116
|
-
"cyan" => [0x00, 0xAA, 0xAA],
|
|
117
|
-
"white" => [0xAA, 0xAA, 0xAA],
|
|
118
|
-
"bright_black" => [0x55, 0x55, 0x55],
|
|
119
|
-
"bright_red" => [0xFF, 0x55, 0x55],
|
|
120
|
-
"bright_green" => [0x55, 0xFF, 0x55],
|
|
121
|
-
"bright_yellow" => [0xFF, 0xFF, 0x55],
|
|
122
|
-
"bright_blue" => [0x55, 0x55, 0xFF],
|
|
123
|
-
"bright_magenta"=> [0xFF, 0x55, 0xFF],
|
|
124
|
-
"bright_cyan" => [0x55, 0xFF, 0xFF],
|
|
125
|
-
"bright_white" => [0xFF, 0xFF, 0xFF],
|
|
126
|
-
}.freeze
|
|
127
|
-
private_constant :ANSI_RGB
|
|
128
|
-
|
|
129
|
-
CUBE = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF].freeze
|
|
130
|
-
private_constant :CUBE
|
|
131
|
-
|
|
132
|
-
ANSI_INDEX = %w[
|
|
133
|
-
black red green yellow blue magenta cyan white
|
|
134
|
-
bright_black bright_red bright_green bright_yellow
|
|
135
|
-
bright_blue bright_magenta bright_cyan bright_white
|
|
136
|
-
].freeze
|
|
137
|
-
private_constant :ANSI_INDEX
|
|
138
|
-
|
|
139
|
-
DEFAULT_FG = [0xC0, 0xC0, 0xC0].freeze
|
|
140
|
-
DEFAULT_BG = [0x00, 0x00, 0x00].freeze
|
|
141
|
-
|
|
142
112
|
def initialize(state)
|
|
143
113
|
@state = state
|
|
144
114
|
@rows = _dig(state, :size, :rows) || 40
|
|
@@ -191,36 +161,6 @@ module TUITD
|
|
|
191
161
|
draw_underline(image, px, py, CELL_W, fg_rgb) if underline
|
|
192
162
|
end
|
|
193
163
|
|
|
194
|
-
def resolve_color(name, fallback)
|
|
195
|
-
case name
|
|
196
|
-
when "default"
|
|
197
|
-
fallback
|
|
198
|
-
when /^#([0-9a-fA-F]{6})$/
|
|
199
|
-
[$1[0..1].to_i(16), $1[2..3].to_i(16), $1[4..5].to_i(16)]
|
|
200
|
-
when /\Acolor(\d+)\z/
|
|
201
|
-
xterm_256($1.to_i)
|
|
202
|
-
when /\Abright_(.+)\z/
|
|
203
|
-
ANSI_RGB[name] || fallback
|
|
204
|
-
else
|
|
205
|
-
ANSI_RGB[name] || fallback
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def xterm_256(index)
|
|
210
|
-
if index < 16
|
|
211
|
-
name = ANSI_INDEX[index]
|
|
212
|
-
ANSI_RGB[name] || DEFAULT_FG
|
|
213
|
-
elsif index < 232
|
|
214
|
-
r = CUBE[((index - 16) / 36) % 6]
|
|
215
|
-
g = CUBE[((index - 16) / 6) % 6]
|
|
216
|
-
b = CUBE[(index - 16) % 6]
|
|
217
|
-
[r, g, b]
|
|
218
|
-
else
|
|
219
|
-
v = 8 + (index - 232) * 10
|
|
220
|
-
[v, v, v]
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
164
|
def fill_rect(image, x, y, w, h, rgb)
|
|
225
165
|
color = ChunkyPNG::Color.rgb(*rgb)
|
|
226
166
|
h.times do |dy|
|
|
@@ -260,12 +200,5 @@ module TUITD
|
|
|
260
200
|
w.times { |dx| image[px + dx, y] = color }
|
|
261
201
|
end
|
|
262
202
|
|
|
263
|
-
def _dig(hash, *keys)
|
|
264
|
-
keys.each do |k|
|
|
265
|
-
return nil unless hash
|
|
266
|
-
hash = hash[k] || hash[k.to_s]
|
|
267
|
-
end
|
|
268
|
-
hash
|
|
269
|
-
end
|
|
270
203
|
end
|
|
271
204
|
end
|
data/lib/tui_td/state.rb
CHANGED
|
@@ -7,6 +7,9 @@ module TUITD
|
|
|
7
7
|
attr_reader :rows, :cols, :grid, :cursor
|
|
8
8
|
|
|
9
9
|
def initialize(data)
|
|
10
|
+
raise ArgumentError, "State data must include :size key" unless data[:size]
|
|
11
|
+
raise ArgumentError, "State data must include :rows key" unless data[:rows]
|
|
12
|
+
|
|
10
13
|
@rows = data[:size][:rows]
|
|
11
14
|
@cols = data[:size][:cols]
|
|
12
15
|
@grid = data[:rows]
|
data/lib/tui_td/test_runner.rb
CHANGED
|
@@ -37,6 +37,12 @@ module TUITD
|
|
|
37
37
|
@plan[:before_all] = @plan[:before_all]&.map { |s| s.transform_keys(&:to_sym) }
|
|
38
38
|
@plan[:after_all] = @plan[:after_all]&.map { |s| s.transform_keys(&:to_sym) }
|
|
39
39
|
@on_step = on_step
|
|
40
|
+
rescue JSON::ParserError => e
|
|
41
|
+
raise Error, "Invalid JSON: #{e.message}"
|
|
42
|
+
@plan[:steps] = @plan[:steps].map { |s| s.transform_keys(&:to_sym) }
|
|
43
|
+
@plan[:before_all] = @plan[:before_all]&.map { |s| s.transform_keys(&:to_sym) }
|
|
44
|
+
@plan[:after_all] = @plan[:after_all]&.map { |s| s.transform_keys(&:to_sym) }
|
|
45
|
+
@on_step = on_step
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def run
|
|
@@ -88,141 +94,98 @@ module TUITD
|
|
|
88
94
|
Result.new(step: action, passed: true, message: "Found: #{value}")
|
|
89
95
|
|
|
90
96
|
when "wait_for_stable"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
when "assert_text"
|
|
96
|
-
ensure_driver!(driver)
|
|
97
|
-
state = State.new(driver.state_data)
|
|
98
|
-
if state.find_text(value.to_s).any?
|
|
99
|
-
Result.new(step: action, passed: true, message: "Text found: #{value}")
|
|
100
|
-
else
|
|
101
|
-
Result.new(step: action, passed: false, message: "Text NOT found: #{value}")
|
|
102
|
-
end
|
|
97
|
+
ensure_driver!(driver)
|
|
98
|
+
driver.wait_for_stable
|
|
99
|
+
Result.new(step: action, passed: true, message: "Stable")
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
state = State.new(driver.state_data)
|
|
107
|
-
if state.find_text(value.to_s).any?
|
|
108
|
-
Result.new(step: action, passed: false, message: "Text found but should not be: #{value}")
|
|
109
|
-
else
|
|
110
|
-
Result.new(step: action, passed: true, message: "Text not found: #{value}")
|
|
111
|
-
end
|
|
101
|
+
when "assert_text", "assert_not_text", "assert_regex"
|
|
102
|
+
check_text(driver, value, action)
|
|
112
103
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
state = State.new(driver.state_data)
|
|
116
|
-
pattern = Regexp.new(value.to_s)
|
|
117
|
-
if state.find_text(pattern).any?
|
|
118
|
-
Result.new(step: action, passed: true, message: "Regex matched: #{value}")
|
|
119
|
-
else
|
|
120
|
-
Result.new(step: action, passed: false, message: "Regex did not match: #{value}")
|
|
121
|
-
end
|
|
104
|
+
when "assert_fg"
|
|
105
|
+
check_color(driver, step, :fg)
|
|
122
106
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
row, col = coords(step)
|
|
126
|
-
expected = step[:is] || step["is"]
|
|
127
|
-
state = State.new(driver.state_data)
|
|
128
|
-
actual = state.foreground_at(row, col)
|
|
129
|
-
if actual == expected
|
|
130
|
-
Result.new(step: action, passed: true, message: "FG at [#{row},#{col}] is #{expected}")
|
|
131
|
-
else
|
|
132
|
-
Result.new(step: action, passed: false, message: "FG at [#{row},#{col}] is #{actual}, expected #{expected}")
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
when "assert_bg"
|
|
136
|
-
ensure_driver!(driver)
|
|
137
|
-
row, col = coords(step)
|
|
138
|
-
expected = step[:is] || step["is"]
|
|
139
|
-
state = State.new(driver.state_data)
|
|
140
|
-
actual = state.background_at(row, col)
|
|
141
|
-
if actual == expected
|
|
142
|
-
Result.new(step: action, passed: true, message: "BG at [#{row},#{col}] is #{expected}")
|
|
143
|
-
else
|
|
144
|
-
Result.new(step: action, passed: false, message: "BG at [#{row},#{col}] is #{actual}, expected #{expected}")
|
|
145
|
-
end
|
|
107
|
+
when "assert_bg"
|
|
108
|
+
check_color(driver, step, :bg)
|
|
146
109
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
110
|
+
when "assert_style"
|
|
111
|
+
ensure_driver!(driver)
|
|
112
|
+
row, col = coords(step)
|
|
113
|
+
state = State.new(driver.state_data)
|
|
114
|
+
actual = state.style_at(row, col)
|
|
115
|
+
expected = {}
|
|
116
|
+
expected[:bold] = step[:bold] unless step[:bold].nil?
|
|
117
|
+
expected[:italic] = step[:italic] unless step[:italic].nil?
|
|
118
|
+
expected[:underline] = step[:underline] unless step[:underline].nil?
|
|
119
|
+
match = expected.all? { |k, v| actual[k] == v }
|
|
120
|
+
if match
|
|
121
|
+
Result.new(step: action, passed: true, message: "Style at [#{row},#{col}] matches #{expected}")
|
|
122
|
+
else
|
|
123
|
+
Result.new(step: action, passed: false, message: "Style at [#{row},#{col}] is #{actual}, expected #{expected}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
when "screenshot"
|
|
127
|
+
ensure_driver!(driver)
|
|
128
|
+
path = value.is_a?(String) ? value : "/tmp/tui_td_#{Time.now.to_i}.png"
|
|
129
|
+
driver.screenshot(path)
|
|
130
|
+
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
162
131
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
when "html"
|
|
170
|
-
ensure_driver!(driver)
|
|
171
|
-
path = value.is_a?(String) ? value : "/tmp/tui_td_#{Time.now.to_i}.html"
|
|
172
|
-
HtmlRenderer.new(driver.state_data).render(path)
|
|
173
|
-
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
174
|
-
|
|
175
|
-
when "wait_for_exit"
|
|
176
|
-
ensure_driver!(driver)
|
|
177
|
-
driver.wait_for_exit
|
|
178
|
-
status = driver.exitstatus
|
|
179
|
-
Result.new(step: action, passed: true, message: "Exited with status #{status}")
|
|
180
|
-
|
|
181
|
-
when "assert_exit"
|
|
182
|
-
ensure_driver!(driver)
|
|
183
|
-
expected = value.to_s.to_i
|
|
184
|
-
actual = driver.exitstatus
|
|
185
|
-
if actual == expected
|
|
186
|
-
Result.new(step: action, passed: true, message: "Exit status #{expected} matches")
|
|
187
|
-
else
|
|
188
|
-
Result.new(step: action, passed: false, message: "Exit status #{actual}, expected #{expected}")
|
|
189
|
-
end
|
|
132
|
+
when "html"
|
|
133
|
+
ensure_driver!(driver)
|
|
134
|
+
path = value.is_a?(String) ? value : "/tmp/tui_td_#{Time.now.to_i}.html"
|
|
135
|
+
HtmlRenderer.new(driver.state_data).render(path)
|
|
136
|
+
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
190
137
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
138
|
+
when "wait_for_exit"
|
|
139
|
+
ensure_driver!(driver)
|
|
140
|
+
driver.wait_for_exit
|
|
141
|
+
status = driver.exitstatus
|
|
142
|
+
Result.new(step: action, passed: true, message: "Exited with status #{status}")
|
|
195
143
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
144
|
+
when "assert_exit"
|
|
145
|
+
ensure_driver!(driver)
|
|
146
|
+
expected = value.to_s.to_i
|
|
147
|
+
actual = driver.exitstatus
|
|
148
|
+
if actual == expected
|
|
149
|
+
Result.new(step: action, passed: true, message: "Exit status #{expected} matches")
|
|
150
|
+
else
|
|
151
|
+
Result.new(step: action, passed: false, message: "Exit status #{actual}, expected #{expected}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
when "close"
|
|
155
|
+
driver&.close
|
|
156
|
+
driver = nil
|
|
157
|
+
Result.new(step: action, passed: true, message: "Closed")
|
|
199
158
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
159
|
+
else
|
|
160
|
+
Result.new(step: action, passed: false, message: "Unknown action: #{action}")
|
|
161
|
+
end
|
|
203
162
|
|
|
204
|
-
|
|
205
|
-
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
r = Result.new(step: action, passed: false, message: "#{e.class}: #{e.message}")
|
|
165
|
+
end
|
|
206
166
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
167
|
+
all_results << r
|
|
168
|
+
all_passed &&= r.passed
|
|
169
|
+
|
|
170
|
+
if @on_step
|
|
171
|
+
state_data = nil
|
|
172
|
+
begin
|
|
173
|
+
state_data = driver.state_data if driver
|
|
174
|
+
rescue StandardError
|
|
175
|
+
# ignore — state retrieval is best-effort
|
|
176
|
+
end
|
|
177
|
+
@on_step.call(
|
|
178
|
+
index: all_results.size - 1,
|
|
179
|
+
total: total_steps,
|
|
180
|
+
action: action,
|
|
181
|
+
value: value,
|
|
182
|
+
result: r,
|
|
183
|
+
driver: driver,
|
|
184
|
+
state_data: state_data
|
|
185
|
+
)
|
|
213
186
|
end
|
|
214
|
-
@on_step.call(
|
|
215
|
-
index: all_results.size - 1,
|
|
216
|
-
total: total_steps,
|
|
217
|
-
action: action,
|
|
218
|
-
value: value,
|
|
219
|
-
result: r,
|
|
220
|
-
driver: driver,
|
|
221
|
-
state_data: state_data
|
|
222
|
-
)
|
|
223
187
|
end
|
|
224
188
|
end
|
|
225
|
-
end
|
|
226
189
|
|
|
227
190
|
driver&.close
|
|
228
191
|
|
|
@@ -241,10 +204,65 @@ module TUITD
|
|
|
241
204
|
|
|
242
205
|
def coords(step)
|
|
243
206
|
pos = step[:assert_fg] || step[:assert_bg] || step[:assert_style]
|
|
244
|
-
pos = value if pos.nil? && (value = step.values.first).is_a?(Array)
|
|
245
207
|
row = pos.is_a?(Array) ? pos[0] : (pos[:row] || pos["row"] || 0)
|
|
246
208
|
col = pos.is_a?(Array) ? pos[1] : (pos[:col] || pos["col"] || 0)
|
|
247
209
|
[row, col]
|
|
248
210
|
end
|
|
211
|
+
|
|
212
|
+
def check_text(driver, value, action)
|
|
213
|
+
ensure_driver!(driver)
|
|
214
|
+
state = State.new(driver.state_data)
|
|
215
|
+
text = value.to_s
|
|
216
|
+
|
|
217
|
+
if action == "assert_regex"
|
|
218
|
+
begin
|
|
219
|
+
pattern = Regexp.new(text)
|
|
220
|
+
rescue RegexpError => e
|
|
221
|
+
return Result.new(step: action, passed: false, message: "Invalid regex: #{e.message}")
|
|
222
|
+
end
|
|
223
|
+
else
|
|
224
|
+
pattern = text
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
found = state.find_text(pattern).any?
|
|
228
|
+
|
|
229
|
+
case action
|
|
230
|
+
when "assert_text"
|
|
231
|
+
if found
|
|
232
|
+
Result.new(step: action, passed: true, message: "Text found: #{value}")
|
|
233
|
+
else
|
|
234
|
+
Result.new(step: action, passed: false, message: "Text NOT found: #{value}")
|
|
235
|
+
end
|
|
236
|
+
when "assert_not_text"
|
|
237
|
+
if found
|
|
238
|
+
Result.new(step: action, passed: false, message: "Text found but should not be: #{value}")
|
|
239
|
+
else
|
|
240
|
+
Result.new(step: action, passed: true, message: "Text not found: #{value}")
|
|
241
|
+
end
|
|
242
|
+
when "assert_regex"
|
|
243
|
+
if found
|
|
244
|
+
Result.new(step: action, passed: true, message: "Regex matched: #{value}")
|
|
245
|
+
else
|
|
246
|
+
Result.new(step: action, passed: false, message: "Regex did not match: #{value}")
|
|
247
|
+
end
|
|
248
|
+
else
|
|
249
|
+
Result.new(step: action, passed: false, message: "Unknown text check: #{action}")
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def check_color(driver, step, property)
|
|
254
|
+
ensure_driver!(driver)
|
|
255
|
+
row, col = coords(step)
|
|
256
|
+
expected = step[:is] || step["is"]
|
|
257
|
+
state = State.new(driver.state_data)
|
|
258
|
+
label = property == :fg ? "FG" : "BG"
|
|
259
|
+
actual = property == :fg ? state.foreground_at(row, col) : state.background_at(row, col)
|
|
260
|
+
|
|
261
|
+
if actual == expected
|
|
262
|
+
Result.new(step: step.keys.first.to_s, passed: true, message: "#{label} at [#{row},#{col}] is #{expected}")
|
|
263
|
+
else
|
|
264
|
+
Result.new(step: step.keys.first.to_s, passed: false, message: "#{label} at [#{row},#{col}] is #{actual}, expected #{expected}")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
249
267
|
end
|
|
250
268
|
end
|
data/lib/tui_td/version.rb
CHANGED
data/lib/tui_td.rb
CHANGED
|
@@ -7,6 +7,7 @@ end
|
|
|
7
7
|
require_relative "tui_td/version"
|
|
8
8
|
require_relative "tui_td/driver"
|
|
9
9
|
require_relative "tui_td/ansi_parser"
|
|
10
|
+
require_relative "tui_td/ansi_utils"
|
|
10
11
|
require_relative "tui_td/state"
|
|
11
12
|
require_relative "tui_td/screenshot"
|
|
12
13
|
require_relative "tui_td/html_renderer"
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haluk Durmus
|
|
@@ -123,6 +123,7 @@ files:
|
|
|
123
123
|
- bin/tui-td
|
|
124
124
|
- lib/tui_td.rb
|
|
125
125
|
- lib/tui_td/ansi_parser.rb
|
|
126
|
+
- lib/tui_td/ansi_utils.rb
|
|
126
127
|
- lib/tui_td/cli.rb
|
|
127
128
|
- lib/tui_td/driver.rb
|
|
128
129
|
- lib/tui_td/html_renderer.rb
|