tui-td 0.2.17 → 0.2.19
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 +29 -0
- data/README.md +52 -0
- data/lib/tui_td/cli.rb +129 -5
- data/lib/tui_td/minitest/assertions.rb +263 -0
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48cd9df54d8ad7bc1d41b8d4cfd961462ecff6e8056c4fd37e46abb56fa50f38
|
|
4
|
+
data.tar.gz: c77b9a6eb3a048806d17d4f5b2245dcf82dd1c5e818a247db48912e9b590d78d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce4bdb6d4397369717686756508ec43219340ed4a0cc0ec8a4851018a19dc19bbdec3050147238a221cd4b282c92d848df1d71a1377b4915bfb42282af400d95
|
|
7
|
+
data.tar.gz: 6053e73dd915f8839d92ef8b046578bae6fbf481b0fda21872851e7e9052816833ce62ab5d6a4d15a685ce517f161fe31e75d5a9309a0dacb3521a3df4e893af
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Named snapshot testing: `match_snapshot("name")` — first run creates golden master,
|
|
8
|
+
subsequent runs compare. Supports `type: :text` (chars_only), `:full`, `:png`, `:html`, `:all`.
|
|
9
|
+
- `ignore_rows:` parameter — skip specific rows during snapshot comparison
|
|
10
|
+
- `region:` parameter — restrict comparison to a row range (e.g., `region: 0..6`)
|
|
11
|
+
Combinable with `ignore_rows:` for fine-grained control.
|
|
12
|
+
- `UPDATE_SNAPSHOTS=1` — environment variable to auto-update all snapshots
|
|
13
|
+
- `TUITD.configure { |c| c.snapshot_dir = "..." }` — configurable snapshot directory
|
|
14
|
+
- `Driver#find_text(pattern, match:)` — convenience delegation to State#find_text
|
|
15
|
+
- `Driver#snapshot` — returns a State snapshot for in-memory comparison
|
|
16
|
+
- JSON test steps: `snapshot`, `assert_snapshot` with `type:` and `wait:` options
|
|
17
|
+
- MCP tools: `tui_save_snapshot`, `tui_assert_snapshot`, `tui_element_actions`,
|
|
18
|
+
`tui_annotate_element`, `tui_diff`
|
|
19
|
+
- Drive mode: `snapshot <name>`, `diff <name>` commands
|
|
20
|
+
- `TUITD::Snapshot` class — core abstraction for save/load/compare of named snapshots
|
|
21
|
+
- `TUITD::Configuration` class — global gem configuration
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `match_snapshot` matcher: now accepts String names (named snapshots) in addition to
|
|
26
|
+
legacy State objects. New kwargs: `type:`, `wait:`, `ignore_rows:`, `region:`.
|
|
27
|
+
- `tui_find_text` MCP tool: added `match:` parameter (`partial`, `exact`, `regex`)
|
|
28
|
+
- `tui_find_elements` MCP tool: added `checked:`/`disabled:` filters, new roles
|
|
29
|
+
- `tui_diff` MCP tool: fixed deep key symbolization for snapshot hash compatibility
|
|
30
|
+
- Dependency: tans-parser `~> 0.1.3`
|
|
31
|
+
|
|
3
32
|
## 0.2.16
|
|
4
33
|
|
|
5
34
|
### Added
|
data/README.md
CHANGED
|
@@ -379,6 +379,56 @@ end
|
|
|
379
379
|
| `have_progress_bar("50%")` | Assert a progress bar (`[####]`) is visible |
|
|
380
380
|
| `have_exit_status(N)` | Assert the driver process exit status equals N |
|
|
381
381
|
|
|
382
|
+
## Snapshot Testing
|
|
383
|
+
|
|
384
|
+
Named, persisted snapshot testing à la Playwright/Jest. First run creates a golden master — subsequent runs compare.
|
|
385
|
+
|
|
386
|
+
### RSpec
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
# Named snapshot (recommended)
|
|
390
|
+
expect(driver).to match_snapshot("login_screen")
|
|
391
|
+
expect(driver).to match_snapshot("login_screen", type: :all, wait: true)
|
|
392
|
+
|
|
393
|
+
# Partial screen comparison
|
|
394
|
+
expect(driver).to match_snapshot("banner", region: 0..6, chars_only: true)
|
|
395
|
+
|
|
396
|
+
# Skip volatile rows (e.g., prompt line)
|
|
397
|
+
expect(driver).to match_snapshot("main", ignore_rows: [5])
|
|
398
|
+
|
|
399
|
+
# In-memory comparison (legacy)
|
|
400
|
+
pre = driver.snapshot
|
|
401
|
+
expect(driver).to match_snapshot(pre, chars_only: true)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Configuration
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
TUITD.configure do |c|
|
|
408
|
+
c.snapshot_dir = "spec/snapshots" # default
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Update all snapshots
|
|
412
|
+
UPDATE_SNAPSHOTS=1 bundle exec rspec
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### JSON Test Steps
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{"snapshot": "login_screen", "type": "text"}
|
|
419
|
+
{"assert_snapshot": "login_screen", "type": "png", "wait": true}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Types
|
|
423
|
+
|
|
424
|
+
| Type | Comparison | File |
|
|
425
|
+
|------|-----------|------|
|
|
426
|
+
| `:text` | chars only (ignores colors/styles) | `.json` |
|
|
427
|
+
| `:full` | full cell comparison (chars + colors) | `.json` |
|
|
428
|
+
| `:png` | screenshot byte-identical | `.png` |
|
|
429
|
+
| `:html` | HTML render byte-identical | `.html` |
|
|
430
|
+
| `:all` | all three at once | `.json` + `.png` + `.html` |
|
|
431
|
+
|
|
382
432
|
## MCP Server — AI Integration
|
|
383
433
|
|
|
384
434
|
Start the MCP server to let any MCP client control TUIs:
|
|
@@ -407,6 +457,8 @@ tui-td serve
|
|
|
407
457
|
| `tui_element_actions` | Get click/type/press_key action hashes for a detected UI element. For AI-driven interaction. |
|
|
408
458
|
| `tui_diff` | Compare current state against a previous snapshot. Returns cell-level differences. |
|
|
409
459
|
| `tui_annotate_element` | Manually register a UI element annotation. Picked up by tui_find_elements. |
|
|
460
|
+
| `tui_save_snapshot` | Save the current terminal state as a named snapshot to disk. |
|
|
461
|
+
| `tui_assert_snapshot` | Assert current state matches a saved named snapshot. Creates on first run. |
|
|
410
462
|
| `tui_close` | Close the TUI and clean up. |
|
|
411
463
|
|
|
412
464
|
### MCP configuration
|
data/lib/tui_td/cli.rb
CHANGED
|
@@ -120,6 +120,8 @@ module TUITD
|
|
|
120
120
|
_help_test
|
|
121
121
|
when "rspec"
|
|
122
122
|
_help_rspec
|
|
123
|
+
when "minitest"
|
|
124
|
+
_help_minitest
|
|
123
125
|
when nil
|
|
124
126
|
_help_main
|
|
125
127
|
else
|
|
@@ -373,8 +375,9 @@ module TUITD
|
|
|
373
375
|
def _help_main
|
|
374
376
|
puts(OptionParser.new { |o| o.banner = "Usage: tui-td <command> [options]" })
|
|
375
377
|
puts
|
|
376
|
-
puts "For more: tui-td help test
|
|
377
|
-
puts " tui-td help rspec
|
|
378
|
+
puts "For more: tui-td help test (JSON test step types)"
|
|
379
|
+
puts " tui-td help rspec (RSpec matchers)"
|
|
380
|
+
puts " tui-td help minitest (Minitest assertions)"
|
|
378
381
|
exit 0
|
|
379
382
|
end
|
|
380
383
|
|
|
@@ -504,6 +507,15 @@ module TUITD
|
|
|
504
507
|
{"assert_progress_bar": true} or {"assert_progress_bar": "50%"}
|
|
505
508
|
Assert that a progress bar ([#### ]) is visible.
|
|
506
509
|
|
|
510
|
+
{"snapshot": "login_screen", "type": "text", "wait": true}
|
|
511
|
+
Save current terminal state as a named snapshot to disk.
|
|
512
|
+
type: "text" (default), "full", "png", "html", "all".
|
|
513
|
+
|
|
514
|
+
{"assert_snapshot": "login_screen", "type": "text", "wait": true}
|
|
515
|
+
Assert current state matches a saved named snapshot.
|
|
516
|
+
First run creates the golden master (passes automatically).
|
|
517
|
+
UPDATE_SNAPSHOTS=1 to force update all snapshots.
|
|
518
|
+
|
|
507
519
|
Example test file: examples/echo_test.json
|
|
508
520
|
HELP
|
|
509
521
|
exit 0
|
|
@@ -616,9 +628,19 @@ module TUITD
|
|
|
616
628
|
Passes if a progress bar ([#### ]) is visible.
|
|
617
629
|
Usage: expect(state).to have_progress_bar
|
|
618
630
|
|
|
619
|
-
match_snapshot(
|
|
620
|
-
|
|
621
|
-
|
|
631
|
+
match_snapshot("<name>", type: :text, wait: false, region: 0..6, ignore_rows: [2])
|
|
632
|
+
Named, persisted snapshot testing. First run creates the snapshot
|
|
633
|
+
(passes automatically), subsequent runs compare.
|
|
634
|
+
Types: :text (chars_only, default), :full (chars+colors), :png, :html, :all.
|
|
635
|
+
region: restricts comparison to a row range (e.g., 0..6).
|
|
636
|
+
ignore_rows: skips specific rows during comparison.
|
|
637
|
+
UPDATE_SNAPSHOTS=1 to auto-update all snapshots.
|
|
638
|
+
Usage: expect(driver).to match_snapshot("login_screen")
|
|
639
|
+
Usage: expect(driver).to match_snapshot("banner", region: 0..6, chars_only: true)
|
|
640
|
+
Usage: expect(driver).to match_snapshot("main", ignore_rows: [5], wait: true)
|
|
641
|
+
|
|
642
|
+
match_snapshot(State, chars_only: false) (legacy in-memory)
|
|
643
|
+
Passes if the current state matches a previously saved snapshot object.
|
|
622
644
|
Usage: pre = driver.snapshot; ... ; expect(driver).to match_snapshot(pre)
|
|
623
645
|
Usage: expect(state).to match_snapshot(snap, chars_only: true)
|
|
624
646
|
|
|
@@ -631,6 +653,108 @@ module TUITD
|
|
|
631
653
|
HELP
|
|
632
654
|
exit 0
|
|
633
655
|
end
|
|
656
|
+
|
|
657
|
+
def _help_minitest
|
|
658
|
+
puts <<~HELP
|
|
659
|
+
Minitest Assertions
|
|
660
|
+
===================
|
|
661
|
+
|
|
662
|
+
Include the assertions module in your Minitest test class:
|
|
663
|
+
|
|
664
|
+
require "tui_td/minitest/assertions"
|
|
665
|
+
|
|
666
|
+
class MyTUITest < Minitest::Test
|
|
667
|
+
include TUITD::Minitest::Assertions
|
|
668
|
+
|
|
669
|
+
def setup
|
|
670
|
+
@driver = TUITD::Driver.new("my_tui", rows: 24, cols: 80)
|
|
671
|
+
@driver.start
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def teardown
|
|
675
|
+
@driver&.close
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
Auto-wait: When given a Driver, assertions wait up to 3 seconds.
|
|
680
|
+
When given a State, assertions check immediately.
|
|
681
|
+
|
|
682
|
+
Assertions
|
|
683
|
+
----------
|
|
684
|
+
|
|
685
|
+
Text / Regex / Color / Style:
|
|
686
|
+
|
|
687
|
+
assert_text(driver, "Welcome")
|
|
688
|
+
Passes if text appears anywhere in the terminal.
|
|
689
|
+
Negate: refute_text(driver, "Error")
|
|
690
|
+
|
|
691
|
+
assert_regex(driver, /error|fail/)
|
|
692
|
+
Passes if regex pattern matches anywhere.
|
|
693
|
+
Negate: refute_regex(driver, /pattern/)
|
|
694
|
+
|
|
695
|
+
assert_fg(driver, "cyan", row: 0, col: 5)
|
|
696
|
+
Assert foreground color at position.
|
|
697
|
+
|
|
698
|
+
assert_bg(driver, "blue", row: 0, col: 0)
|
|
699
|
+
Assert background color at position.
|
|
700
|
+
|
|
701
|
+
assert_style(driver, row: 0, col: 0, bold: true, italic: false)
|
|
702
|
+
Assert style attributes at position.
|
|
703
|
+
|
|
704
|
+
assert_exit_status(driver, 0)
|
|
705
|
+
Assert the process exit status matches expected.
|
|
706
|
+
|
|
707
|
+
Selector assertions:
|
|
708
|
+
|
|
709
|
+
assert_button(driver, "OK")
|
|
710
|
+
Passes if a button with given text is visible.
|
|
711
|
+
Negate: refute_button(driver, "Cancel")
|
|
712
|
+
|
|
713
|
+
assert_dialog(driver)
|
|
714
|
+
Passes if a dialog (box-drawing region) is visible.
|
|
715
|
+
Negate: refute_dialog(driver)
|
|
716
|
+
|
|
717
|
+
assert_checkbox(driver, "Label", checked: true)
|
|
718
|
+
Passes if checkbox with given label (and optionally state) is visible.
|
|
719
|
+
Use checked: true, checked: false, or unchecked: true.
|
|
720
|
+
|
|
721
|
+
assert_role(driver, :button, text: "OK", checked: nil, disabled: nil)
|
|
722
|
+
Generic role assertion. Accepts :button, :checkbox, :dialog,
|
|
723
|
+
:statusbar, :progress, :input, :label, :menu, :tab.
|
|
724
|
+
|
|
725
|
+
assert_input(driver)
|
|
726
|
+
assert_input(driver, "Name")
|
|
727
|
+
Passes if an input field ([____]) is visible.
|
|
728
|
+
|
|
729
|
+
assert_label(driver, "Username")
|
|
730
|
+
Passes if a label (text ending with colon) is visible.
|
|
731
|
+
|
|
732
|
+
assert_menu(driver)
|
|
733
|
+
Passes if a menu bar or dropdown item is visible.
|
|
734
|
+
|
|
735
|
+
assert_tab(driver, "File")
|
|
736
|
+
Passes if a tab is visible.
|
|
737
|
+
|
|
738
|
+
assert_statusbar(driver)
|
|
739
|
+
Passes if a status bar (bottom row with background) is visible.
|
|
740
|
+
|
|
741
|
+
assert_progress_bar(driver, "50%")
|
|
742
|
+
Passes if a progress bar ([#### ]) is visible.
|
|
743
|
+
|
|
744
|
+
Snapshot:
|
|
745
|
+
|
|
746
|
+
assert_snapshot(driver, "login_screen", type: :text, wait: true)
|
|
747
|
+
Named snapshot testing. First run creates golden master,
|
|
748
|
+
subsequent runs compare.
|
|
749
|
+
|
|
750
|
+
assert_snapshot(driver, "banner", region: 0..6, chars_only: true)
|
|
751
|
+
Partial screen comparison with region:.
|
|
752
|
+
|
|
753
|
+
assert_snapshot(driver, "main", ignore_rows: [5])
|
|
754
|
+
Skip volatile rows with ignore_rows:.
|
|
755
|
+
HELP
|
|
756
|
+
exit 0
|
|
757
|
+
end
|
|
634
758
|
end
|
|
635
759
|
end
|
|
636
760
|
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
|
|
4
|
+
|
|
5
|
+
require "minitest"
|
|
6
|
+
|
|
7
|
+
module TUITD
|
|
8
|
+
module Minitest
|
|
9
|
+
# Assertions for TUI testing with Minitest.
|
|
10
|
+
#
|
|
11
|
+
# Include this module in your Minitest test class:
|
|
12
|
+
#
|
|
13
|
+
# require "tui_td/minitest/assertions"
|
|
14
|
+
#
|
|
15
|
+
# class MyTUITest < Minitest::Test
|
|
16
|
+
# include TUITD::Minitest::Assertions
|
|
17
|
+
#
|
|
18
|
+
# def test_login_screen
|
|
19
|
+
# driver = TUITD::Driver.new("my_tui", rows: 24, cols: 80)
|
|
20
|
+
# driver.start
|
|
21
|
+
# assert_text(driver, "Welcome")
|
|
22
|
+
# assert_button(driver, "OK")
|
|
23
|
+
# refute_text(driver, "Error")
|
|
24
|
+
# ensure
|
|
25
|
+
# driver&.close
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# Auto-wait: When given a Driver, assertions wait up to 3 seconds.
|
|
30
|
+
# When given a State, assertions check immediately.
|
|
31
|
+
#
|
|
32
|
+
module Assertions
|
|
33
|
+
AUTO_WAIT_TIMEOUT = 3
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def auto_wait(actual, timeout: AUTO_WAIT_TIMEOUT, &predicate)
|
|
38
|
+
if actual.respond_to?(:wait_for)
|
|
39
|
+
begin
|
|
40
|
+
actual.wait_for(timeout: timeout, &predicate)
|
|
41
|
+
true
|
|
42
|
+
rescue TUITD::TimeoutError
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
predicate.call(actual)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def state_from(actual)
|
|
51
|
+
if actual.respond_to?(:state_data)
|
|
52
|
+
TUITD::State.new(actual.state_data)
|
|
53
|
+
else
|
|
54
|
+
actual # State or raw hash — pass through
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
public
|
|
59
|
+
|
|
60
|
+
# --- Text / Regex / Color / Style ---
|
|
61
|
+
|
|
62
|
+
def assert_text(actual, expected)
|
|
63
|
+
result = auto_wait(actual) { |s| s.find_text(expected).any? }
|
|
64
|
+
assert(result, "Expected terminal to contain #{expected.inspect}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def refute_text(actual, expected)
|
|
68
|
+
result = auto_wait(actual) { |s| s.find_text(expected).empty? }
|
|
69
|
+
assert(result, "Expected terminal NOT to contain #{expected.inspect}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def assert_regex(actual, pattern)
|
|
73
|
+
regex = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern.to_s)
|
|
74
|
+
result = auto_wait(actual) { |s| s.find_text(regex).any? }
|
|
75
|
+
assert(result, "Expected terminal to match #{pattern.inspect}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def refute_regex(actual, pattern)
|
|
79
|
+
regex = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern.to_s)
|
|
80
|
+
result = auto_wait(actual) { |s| s.find_text(regex).empty? }
|
|
81
|
+
assert(result, "Expected terminal NOT to match #{pattern.inspect}")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def assert_fg(actual, expected, row:, col:)
|
|
85
|
+
result = auto_wait(actual) { |s| s.foreground_at(row, col) == expected }
|
|
86
|
+
actual_fg = state_from(actual).foreground_at(row, col)
|
|
87
|
+
assert(result, "Expected FG at [#{row},#{col}] to be #{expected.inspect}, but was #{actual_fg.inspect}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def assert_bg(actual, expected, row:, col:)
|
|
91
|
+
result = auto_wait(actual) { |s| s.background_at(row, col) == expected }
|
|
92
|
+
actual_bg = state_from(actual).background_at(row, col)
|
|
93
|
+
assert(result, "Expected BG at [#{row},#{col}] to be #{expected.inspect}, but was #{actual_bg.inspect}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def assert_style(actual, row:, col:, **expected_styles)
|
|
97
|
+
result = auto_wait(actual) do |s|
|
|
98
|
+
style = s.style_at(row, col)
|
|
99
|
+
expected_styles.all? { |k, v| style[k] == v }
|
|
100
|
+
end
|
|
101
|
+
actual_style = state_from(actual).style_at(row, col)
|
|
102
|
+
assert(result,
|
|
103
|
+
"Expected style at [#{row},#{col}] to be #{expected_styles.inspect}, but was #{actual_style.inspect}",)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def assert_exit_status(actual, expected)
|
|
107
|
+
status = actual.exitstatus
|
|
108
|
+
assert(status == expected, "Expected exit status #{expected}, but was #{status}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# --- Selector-based ---
|
|
112
|
+
|
|
113
|
+
def assert_button(actual, expected)
|
|
114
|
+
result = auto_wait(actual) { |s| TUITD::Selector.new(s).button(text: expected) }
|
|
115
|
+
assert(result, "Expected terminal to have a button #{expected.inspect}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def refute_button(actual, expected)
|
|
119
|
+
result = auto_wait(actual) { |s| TUITD::Selector.new(s).button(text: expected).nil? }
|
|
120
|
+
assert(result, "Expected terminal NOT to have a button #{expected.inspect}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def assert_dialog(actual)
|
|
124
|
+
result = auto_wait(actual) { |s| TUITD::Selector.new(s).dialogs.any? }
|
|
125
|
+
assert(result, "Expected terminal to have a dialog")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def refute_dialog(actual)
|
|
129
|
+
result = auto_wait(actual) { |s| TUITD::Selector.new(s).dialogs.empty? }
|
|
130
|
+
assert(result, "Expected terminal NOT to have a dialog")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def assert_checkbox(actual, expected, checked: nil, unchecked: nil)
|
|
134
|
+
checked = false if unchecked
|
|
135
|
+
result = auto_wait(actual) do |s|
|
|
136
|
+
filters = { text: expected }
|
|
137
|
+
filters[:checked] = checked unless checked.nil?
|
|
138
|
+
TUITD::Selector.new(s).checkbox(**filters)
|
|
139
|
+
end
|
|
140
|
+
msg = "Expected terminal to have checkbox #{expected.inspect}"
|
|
141
|
+
msg += " (checked)" if checked == true
|
|
142
|
+
msg += " (unchecked)" if checked == false
|
|
143
|
+
assert(result, msg)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def assert_role(actual, role, text: nil, checked: nil, disabled: nil)
|
|
147
|
+
result = auto_wait(actual) do |s|
|
|
148
|
+
filters = {}
|
|
149
|
+
filters[:text] = text if text
|
|
150
|
+
filters[:checked] = checked unless checked.nil?
|
|
151
|
+
filters[:disabled] = disabled unless disabled.nil?
|
|
152
|
+
TUITD::Selector.new(s).get_by_role(role, **filters).any?
|
|
153
|
+
end
|
|
154
|
+
msg = "Expected terminal to have role :#{role}"
|
|
155
|
+
msg += " with text #{text.inspect}" if text
|
|
156
|
+
assert(result, msg)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def assert_input(actual, expected = nil)
|
|
160
|
+
result = auto_wait(actual) do |s|
|
|
161
|
+
if expected
|
|
162
|
+
TUITD::Selector.new(s).input(text: expected)
|
|
163
|
+
else
|
|
164
|
+
TUITD::Selector.new(s).inputs.any?
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
msg = "Expected terminal to have an input field"
|
|
168
|
+
msg += " #{expected.inspect}" if expected
|
|
169
|
+
assert(result, msg)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def assert_label(actual, expected = nil)
|
|
173
|
+
result = auto_wait(actual) do |s|
|
|
174
|
+
if expected
|
|
175
|
+
TUITD::Selector.new(s).label(text: expected)
|
|
176
|
+
else
|
|
177
|
+
TUITD::Selector.new(s).labels.any?
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
msg = "Expected terminal to have a label"
|
|
181
|
+
msg += " #{expected.inspect}" if expected
|
|
182
|
+
assert(result, msg)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def assert_menu(actual, expected = nil)
|
|
186
|
+
result = auto_wait(actual) do |s|
|
|
187
|
+
if expected
|
|
188
|
+
TUITD::Selector.new(s).menu(text: expected)
|
|
189
|
+
else
|
|
190
|
+
TUITD::Selector.new(s).menus.any?
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
msg = "Expected terminal to have a menu"
|
|
194
|
+
msg += " #{expected.inspect}" if expected
|
|
195
|
+
assert(result, msg)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def assert_tab(actual, expected = nil)
|
|
199
|
+
result = auto_wait(actual) do |s|
|
|
200
|
+
if expected
|
|
201
|
+
TUITD::Selector.new(s).tab(text: expected)
|
|
202
|
+
else
|
|
203
|
+
TUITD::Selector.new(s).tabs.any?
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
msg = "Expected terminal to have a tab"
|
|
207
|
+
msg += " #{expected.inspect}" if expected
|
|
208
|
+
assert(result, msg)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def assert_statusbar(actual, expected = nil)
|
|
212
|
+
result = auto_wait(actual) do |s|
|
|
213
|
+
if expected
|
|
214
|
+
TUITD::Selector.new(s).statusbar(text: expected)
|
|
215
|
+
else
|
|
216
|
+
TUITD::Selector.new(s).statusbars.any?
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
msg = "Expected terminal to have a status bar"
|
|
220
|
+
msg += " #{expected.inspect}" if expected
|
|
221
|
+
assert(result, msg)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def assert_progress_bar(actual, expected = nil)
|
|
225
|
+
result = auto_wait(actual) do |s|
|
|
226
|
+
if expected
|
|
227
|
+
TUITD::Selector.new(s).progress_bar(text: expected)
|
|
228
|
+
else
|
|
229
|
+
TUITD::Selector.new(s).progress_bars.any?
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
msg = "Expected terminal to have a progress bar"
|
|
233
|
+
msg += " #{expected.inspect}" if expected
|
|
234
|
+
assert(result, msg)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# --- Snapshot ---
|
|
238
|
+
|
|
239
|
+
def assert_snapshot(actual, name, type: :text, wait: false, region: nil, ignore_rows: nil)
|
|
240
|
+
snap = TUITD::Snapshot.new(name.to_s, type: type)
|
|
241
|
+
|
|
242
|
+
state_data = if actual.respond_to?(:state_data)
|
|
243
|
+
actual.wait_for_stable if wait && actual.respond_to?(:wait_for_stable)
|
|
244
|
+
actual.state_data
|
|
245
|
+
elsif actual.respond_to?(:to_h)
|
|
246
|
+
actual.to_h
|
|
247
|
+
else
|
|
248
|
+
actual
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if TUITD.configuration.update_snapshots? || !snap.exists?
|
|
252
|
+
snap.save(state_data)
|
|
253
|
+
pass
|
|
254
|
+
else
|
|
255
|
+
result = snap.compare(state_data, ignore_rows: ignore_rows, region: region)
|
|
256
|
+
msg = result.passed? ? nil : "Snapshot '#{name}' does not match.\n#{result.message}"
|
|
257
|
+
assert(result.passed?, msg)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
|
data/lib/tui_td/version.rb
CHANGED
data/lib/tui_td.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "tui_td/screenshot"
|
|
|
19
19
|
require_relative "tui_td/html_renderer"
|
|
20
20
|
require_relative "tui_td/test_runner"
|
|
21
21
|
require_relative "tui_td/selector"
|
|
22
|
+
require_relative "tui_td/minitest/assertions"
|
|
22
23
|
require_relative "tui_td/mcp/server"
|
|
23
24
|
require_relative "tui_td/cli"
|
|
24
25
|
|
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.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haluk Durmus
|
|
@@ -151,7 +151,8 @@ dependencies:
|
|
|
151
151
|
version: '1.50'
|
|
152
152
|
description: tui-td drives terminal applications in a PTY, captures ANSI state as
|
|
153
153
|
structured data, and provides PNG screenshots and HTML renders. Includes an MCP
|
|
154
|
-
server for AI-driven testing, a JSON test runner,
|
|
154
|
+
server for AI-driven testing, a JSON test runner, RSpec matchers, Minitest assertions,
|
|
155
|
+
semantic selectors, and named snapshot testing.
|
|
155
156
|
email:
|
|
156
157
|
- haluk_durmus@yahoo.de
|
|
157
158
|
executables:
|
|
@@ -173,6 +174,7 @@ files:
|
|
|
173
174
|
- lib/tui_td/html_renderer.rb
|
|
174
175
|
- lib/tui_td/matchers.rb
|
|
175
176
|
- lib/tui_td/mcp/server.rb
|
|
177
|
+
- lib/tui_td/minitest/assertions.rb
|
|
176
178
|
- lib/tui_td/screenshot.rb
|
|
177
179
|
- lib/tui_td/selector.rb
|
|
178
180
|
- lib/tui_td/snapshot.rb
|
|
@@ -204,5 +206,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
204
206
|
requirements: []
|
|
205
207
|
rubygems_version: 4.0.6
|
|
206
208
|
specification_version: 4
|
|
207
|
-
summary: TUI testing framework —
|
|
209
|
+
summary: TUI testing framework — RSpec, Minitest, JSON, and MCP
|
|
208
210
|
test_files: []
|