tui-td 0.2.2 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b74325984554c843a191d5affcf77472229000da6f14551c26ca2e6f1b0425cd
4
- data.tar.gz: 2ddeb1aaedfc23ca62e3e791bb254950ef4432a32f0031e0a144f7567e2a3785
3
+ metadata.gz: 97ed62eebe255ca568235b0ffe8bb428812d0dbc0408e24e768d76e8b11beb8a
4
+ data.tar.gz: 8b66334f5408398dab8dcf02535e8473c2d9af7357f4fc1db45bc67ca792706c
5
5
  SHA512:
6
- metadata.gz: b02944c10ed90a607ccfadd80fe80237a1bd5d50bba8e92320875c3cc71181f04057d2cad78648f5ddefee7a149b179c215c12b0ed61a19af7363b658090e36a
7
- data.tar.gz: fe0f2b747cc15fe39da0195c83371ef44c457194f1648aa5e4e03f525ac856a60f3561a482012009054158350d354ea7cf28a04ab75eb9d660c1712203364373
6
+ metadata.gz: 404c5b2c0c411b25846855d0d5ad297493be90672ee3eba53980df429ec1263170b605d023983a4fbc17c2b4563b10ff91c98324a08651cf0997d4a9cde21667
7
+ data.tar.gz: 5b73ca7cccaa81b434ca418e5f246573033ac5abd6c18b7094adad6f65d8fa453be2e8c84d842f2ba82e182a9189989a2389599874e1ab9448606db30677d901
data/CHANGELOG.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.2.4
4
+
5
+ - `assert_regex` JSON test step — match terminal output against a Ruby regex
6
+ - `assert_not_text` JSON test step — fail if text IS present (inverse of assert_text)
7
+ - `have_regex` RSpec matcher — regex assertions in spec files
8
+ - `have_text` negation in RSpec: `expect(state).not_to have_text("Error")`
9
+ - README step reference table updated (all 13 step types listed)
10
+
11
+ ## 0.2.3
12
+
13
+ - `have_exit_status` RSpec matcher and `exitstatus` drive command — exit code testing on all three levels (JSON + RSpec + drive)
14
+ - `env` in start step — inject environment variables per test run
15
+ - Per-step `timeout` override
16
+ - `before_all` / `after_all` hooks for setup and teardown steps
17
+
3
18
  ## 0.2.2
4
19
 
5
- - `wait_for_exit` and `assert_exit` test steps — test process exit codes
20
+ - `wait_for_exit` and `assert_exit` JSON test steps — test process exit codes
6
21
 
7
22
  ## 0.2.1
8
23
 
data/README.md CHANGED
@@ -120,6 +120,7 @@ Interactive commands (drive mode):
120
120
  key <name> Send keystroke (enter, tab, escape, up, down, left, right,
121
121
  backspace, ctrl_c, ctrl_d)
122
122
  <text> Send text to the TUI
123
+ exitstatus Show process exit status (nil if running)
123
124
  exit Quit drive mode
124
125
 
125
126
  Global options:
@@ -291,9 +292,13 @@ tui-td test examples/echo_test.json
291
292
  | `wait_for_text` | `"text"` | Wait until text appears |
292
293
  | `wait_for_stable` | — | Wait until output is stable |
293
294
  | `assert_text` | `"text"` | Assert that text exists on screen |
295
+ | `assert_not_text` | `"text"` | Assert that text does NOT exist on screen |
296
+ | `assert_regex` | `"pattern"` | Assert that regex pattern matches (e.g. `"error\|fail"`) |
294
297
  | `assert_fg` | `[row, col], "is": "color"` | Assert foreground color |
295
298
  | `assert_bg` | `[row, col], "is": "color"` | Assert background color |
296
299
  | `assert_style` | `[row, col], "bold": true` | Assert cell style (bold, italic, underline) |
300
+ | `wait_for_exit` | — | Wait until the process exits |
301
+ | `assert_exit` | `N` | Assert the process exit code equals N |
297
302
  | `screenshot` | `"path"` | Save PNG screenshot |
298
303
  | `html` | `"path"` | Save HTML render for browser viewing |
299
304
  | `close` | — | Close the TUI |
data/lib/tui_td/cli.rb CHANGED
@@ -41,6 +41,7 @@ module TUITD
41
41
  opts.separator " key <name> Send keystroke (enter, tab, escape, up, down, left, right,"
42
42
  opts.separator " backspace, ctrl_c, ctrl_d)"
43
43
  opts.separator " <text> Send text to the TUI"
44
+ opts.separator " exitstatus Show process exit status (nil if running)"
44
45
  opts.separator " exit Quit drive mode"
45
46
  opts.separator ""
46
47
  opts.separator "Global options:"
@@ -188,6 +189,9 @@ module TUITD
188
189
  puts driver.state_json(pretty: true)
189
190
  elsif input == "raw"
190
191
  puts driver.raw_output[0..2000]
192
+ elsif input == "exitstatus"
193
+ status = driver.exitstatus
194
+ puts status ? "Exit status: #{status}" : "Process still running"
191
195
  elsif input.start_with?("key ")
192
196
  driver.send_keys(input.split(" ", 2).last.to_sym)
193
197
  else
@@ -327,12 +331,24 @@ module TUITD
327
331
  A test is a Hash or JSON string: {"name": "...", "steps": [...]}
328
332
 
329
333
  Top-level keys: name, steps, rows (default 40), cols (default 120),
330
- timeout (default 30), chdir
334
+ timeout (default 30), chdir, before_all, after_all
335
+
336
+ before_all / after_all are arrays of steps that run before and
337
+ after the main steps list. Useful for setup/teardown:
338
+
339
+ "before_all": [{"start": "my_tui"}, {"wait_for_text": "> "}],
340
+ "steps": [{"send": "hello\\n"}],
341
+ "after_all": [{"close": true}]
342
+
343
+ Each step can also set a per-step "timeout" (in seconds):
344
+
345
+ {"wait_for_text": "Slow", "timeout": 60}
331
346
 
332
347
  Each step is an object with a single action key:
333
348
 
334
349
  {"start": "<command>"}
335
- Start a TUI process in a PTY.
350
+ Start a TUI process in a PTY. Environment variables can be
351
+ passed via "env": {"FOO": "bar", "BAZ": "qux"}.
336
352
 
337
353
  {"send": "<text>"}
338
354
  Send text to the TUI. Use "\\n" for Enter.
@@ -350,6 +366,13 @@ module TUITD
350
366
  {"assert_text": "<substring>"}
351
367
  Fail if the text is not found in the current state.
352
368
 
369
+ {"assert_not_text": "<substring>"}
370
+ Fail if the text IS found in the current state.
371
+
372
+ {"assert_regex": "<pattern>"}
373
+ Fail if the regex pattern does not match anywhere.
374
+ Pattern syntax is Ruby regex (e.g. "error|fail|warn").
375
+
353
376
  {"assert_fg": [row, col], "is": "<color>"}
354
377
  Assert foreground color at cell. Colors: "default",
355
378
  named ANSI (red, green, blue, cyan, ...), "bright_*",
@@ -416,6 +439,13 @@ module TUITD
416
439
  have_text(expected)
417
440
  Passes if expected text appears anywhere in the terminal state.
418
441
  Usage: expect(state).to have_text("Hello")
442
+ Negate: expect(state).not_to have_text("Error")
443
+
444
+ have_regex(pattern)
445
+ Passes if the regex pattern matches anywhere. Accepts a Regexp
446
+ or a string (parsed as Ruby regex).
447
+ Usage: expect(state).to have_regex(/error|fail/)
448
+ Usage: expect(state).to have_regex("\\d{3}")
419
449
 
420
450
  have_fg(expected).at(row, col)
421
451
  Assert foreground color at [row, col] matches expected.
@@ -428,6 +458,13 @@ module TUITD
428
458
  have_style.at(row, col).with(bold: true, italic: false, ...)
429
459
  Assert style attributes at [row, col] match the given hash.
430
460
  Usage: expect(state).to have_style.at(0, 0).with(bold: true)
461
+
462
+ Driver matchers (work on TUITD::Driver, not State)
463
+ --------------------------------------------------
464
+
465
+ have_exit_status(expected)
466
+ Assert the process exit status matches expected.
467
+ Usage: expect(driver).to have_exit_status(0)
431
468
  HELP
432
469
  exit 0
433
470
  end
data/lib/tui_td/driver.rb CHANGED
@@ -19,12 +19,13 @@ module TUITD
19
19
  class Driver
20
20
  attr_reader :command, :state
21
21
 
22
- def initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil)
22
+ def initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil, env: {})
23
23
  @command = command
24
24
  @rows = rows
25
25
  @cols = cols
26
26
  @timeout = timeout
27
27
  @chdir = chdir
28
+ @env = env
28
29
  @state = nil
29
30
  @stdin = nil
30
31
  @stdout = nil
@@ -37,7 +38,7 @@ module TUITD
37
38
 
38
39
  # Start the TUI application in a PTY
39
40
  def start
40
- env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s, "LINES" => @rows.to_s }
41
+ env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s, "LINES" => @rows.to_s }.merge(@env.transform_keys(&:to_s))
41
42
  spawn_opts = {}
42
43
  spawn_opts[:chdir] = @chdir if @chdir
43
44
 
@@ -23,6 +23,17 @@ module TUITD
23
23
  failure_message_when_negated { |state| "expected terminal NOT to contain #{expected.inspect}" }
24
24
  end
25
25
 
26
+ RSpec::Matchers.define :have_regex do |pattern|
27
+ match do |state|
28
+ @regex = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern.to_s)
29
+ state.find_text(@regex).any?
30
+ end
31
+
32
+ description { "match regex #{pattern.inspect}" }
33
+ failure_message { |state| "expected terminal to match #{pattern.inspect}" }
34
+ failure_message_when_negated { |state| "expected terminal NOT to match #{pattern.inspect}" }
35
+ end
36
+
26
37
  RSpec::Matchers.define :have_fg do |expected|
27
38
  chain(:at) { |row, col| @row, @col = row, col }
28
39
 
@@ -68,5 +79,21 @@ module TUITD
68
79
  "expected style at [#{@row},#{@col}] to be #{@expected.inspect}, but was #{@actual.inspect}"
69
80
  end
70
81
  end
82
+
83
+ # Works on a Driver instance, not State
84
+ RSpec::Matchers.define :have_exit_status do |expected|
85
+ match do |driver|
86
+ @actual = driver.exitstatus
87
+ @actual == expected
88
+ end
89
+
90
+ description { "have exit status #{expected}" }
91
+ failure_message do |driver|
92
+ "expected exit status #{expected}, but was #{@actual}"
93
+ end
94
+ failure_message_when_negated do |driver|
95
+ "expected exit status not to be #{expected}"
96
+ end
97
+ end
71
98
  end
72
99
  end
@@ -13,16 +13,20 @@ module TUITD
13
13
  # {
14
14
  # "name": "My test",
15
15
  # "rows": 24, "cols": 80, "timeout": 10,
16
+ # "chdir": "/path/to/workdir",
17
+ # "before_all": [{"start": "my_tui", "env": {"FOO": "bar"}}],
16
18
  # "steps": [
17
- # {"start": "my_tui"},
18
19
  # {"wait_for_text": "> "},
19
20
  # {"send": "hello\n"},
20
21
  # {"assert_text": "hello"},
21
- # {"assert_fg": [0, 0], "is": "cyan"},
22
- # {"close": true}
23
- # ]
22
+ # {"assert_fg": [0, 0], "is": "cyan"}
23
+ # ],
24
+ # "after_all": [{"close": true}]
24
25
  # }
25
26
  #
27
+ # Per-step "timeout" overrides the top-level default:
28
+ # {"wait_for_text": "Slow", "timeout": 60}
29
+ #
26
30
  class TestRunner
27
31
  Result = Struct.new(:step, :passed, :message, keyword_init: true)
28
32
 
@@ -30,47 +34,60 @@ module TUITD
30
34
  raw = source.is_a?(String) ? JSON.parse(source) : source
31
35
  @plan = raw.transform_keys(&:to_sym)
32
36
  @plan[:steps] = @plan[:steps].map { |s| s.transform_keys(&:to_sym) }
37
+ @plan[:before_all] = @plan[:before_all]&.map { |s| s.transform_keys(&:to_sym) }
38
+ @plan[:after_all] = @plan[:after_all]&.map { |s| s.transform_keys(&:to_sym) }
33
39
  @on_step = on_step
34
40
  end
35
41
 
36
42
  def run
37
- results = []
38
- all_passed = true
39
43
  driver = nil
40
44
  rows = @plan[:rows] || 40
41
45
  cols = @plan[:cols] || 120
42
46
  timeout = @plan[:timeout] || 30
43
47
  chdir = @plan[:chdir]
44
48
 
45
- @plan[:steps].each do |step|
46
- action = step.keys.first.to_s
47
- value = step.values.first
48
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
-
50
- begin
51
- r = case action
52
- when "start"
53
- driver&.close
54
- driver = Driver.new(value.to_s, rows: rows, cols: cols, timeout: timeout, chdir: chdir)
55
- driver.start
56
- Result.new(step: action, passed: true, message: "Started: #{value}")
57
-
58
- when "send"
59
- ensure_driver!(driver)
60
- driver.send(value.to_s)
61
- Result.new(step: action, passed: true, message: "Sent #{value.to_s.length} characters")
49
+ hooks = [
50
+ { label: :before_all, steps: @plan[:before_all] || [] },
51
+ { label: :main, steps: @plan[:steps] },
52
+ { label: :after_all, steps: @plan[:after_all] || [] }
53
+ ]
62
54
 
63
- when "send_key"
64
- ensure_driver!(driver)
65
- driver.send_keys(value.to_s.to_sym)
66
- Result.new(step: action, passed: true, message: "Sent key: #{value}")
55
+ all_results = []
56
+ all_passed = true
57
+ total_steps = hooks.sum { |p| p[:steps].size }
67
58
 
68
- when "wait_for_text"
69
- ensure_driver!(driver)
70
- driver.wait_for_text(value.to_s)
71
- Result.new(step: action, passed: true, message: "Found: #{value}")
59
+ hooks.each do |phase|
60
+ phase[:steps].each do |step|
61
+ action = step.keys.first.to_s
62
+ value = step.values.first
72
63
 
73
- when "wait_for_stable"
64
+ begin
65
+ step_timeout = step[:timeout] || timeout
66
+ r = case action
67
+ when "start"
68
+ driver&.close
69
+ env = step[:env] || {}
70
+ env = env.transform_keys(&:to_sym).transform_values(&:to_s) if env.is_a?(Hash)
71
+ driver = Driver.new(value.to_s, rows: rows, cols: cols, timeout: step_timeout, chdir: chdir, env: env)
72
+ driver.start
73
+ Result.new(step: action, passed: true, message: "Started: #{value}")
74
+
75
+ when "send"
76
+ ensure_driver!(driver)
77
+ driver.send(value.to_s)
78
+ Result.new(step: action, passed: true, message: "Sent #{value.to_s.length} characters")
79
+
80
+ when "send_key"
81
+ ensure_driver!(driver)
82
+ driver.send_keys(value.to_s.to_sym)
83
+ Result.new(step: action, passed: true, message: "Sent key: #{value}")
84
+
85
+ when "wait_for_text"
86
+ ensure_driver!(driver)
87
+ driver.wait_for_text(value.to_s)
88
+ Result.new(step: action, passed: true, message: "Found: #{value}")
89
+
90
+ when "wait_for_stable"
74
91
  ensure_driver!(driver)
75
92
  driver.wait_for_stable
76
93
  Result.new(step: action, passed: true, message: "Stable")
@@ -84,6 +101,25 @@ module TUITD
84
101
  Result.new(step: action, passed: false, message: "Text NOT found: #{value}")
85
102
  end
86
103
 
104
+ when "assert_not_text"
105
+ ensure_driver!(driver)
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
112
+
113
+ when "assert_regex"
114
+ ensure_driver!(driver)
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
122
+
87
123
  when "assert_fg"
88
124
  ensure_driver!(driver)
89
125
  row, col = coords(step)
@@ -165,7 +201,7 @@ module TUITD
165
201
  r = Result.new(step: action, passed: false, message: "#{e.class}: #{e.message}")
166
202
  end
167
203
 
168
- results << r
204
+ all_results << r
169
205
  all_passed &&= r.passed
170
206
 
171
207
  if @on_step
@@ -176,8 +212,8 @@ module TUITD
176
212
  # ignore — state retrieval is best-effort
177
213
  end
178
214
  @on_step.call(
179
- index: results.size - 1,
180
- total: @plan[:steps].size,
215
+ index: all_results.size - 1,
216
+ total: total_steps,
181
217
  action: action,
182
218
  value: value,
183
219
  result: r,
@@ -186,13 +222,14 @@ module TUITD
186
222
  )
187
223
  end
188
224
  end
225
+ end
189
226
 
190
227
  driver&.close
191
228
 
192
229
  {
193
230
  name: @plan[:name] || "(unnamed)",
194
231
  passed: all_passed,
195
- results: results.map(&:to_h)
232
+ results: all_results.map(&:to_h)
196
233
  }
197
234
  end
198
235
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TUITD
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  end
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.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Haluk Durmus