tui-td 0.2.13 → 0.2.17
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 +63 -0
- data/README.md +24 -6
- data/lib/tui_td/cli.rb +103 -2
- data/lib/tui_td/configuration.rb +33 -0
- data/lib/tui_td/driver.rb +13 -0
- data/lib/tui_td/matchers.rb +258 -12
- data/lib/tui_td/mcp/server.rb +262 -7
- data/lib/tui_td/selector.rb +0 -23
- data/lib/tui_td/snapshot.rb +247 -0
- data/lib/tui_td/test_runner.rb +65 -6
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +2 -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: cadf6d0ad5e362e3704a7a87673e0ad7254573eba2f53575aea38fc45f949877
|
|
4
|
+
data.tar.gz: fbfd589b84e911d6c46b16976b34dc46943019c52bd7519b04ffc6422a71b659
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c0e8f58b7f193a556d2dee0e1f8c15f005b669fe3823d31763e57efd8c35d511f5e2e5dbb9ff03a61abe534030c1673cc1de437bcbe705b8c008c1ff9af8878a
|
|
7
|
+
data.tar.gz: 520f31800ee26803d9ec56b9128b729f59a057246a11e2b70c1647f253b76bb95e06f99631fb4d6c23597415e8a48a92725e22c36b99faca932cb75d00c84d8c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.16
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Driver#find_text: convenience delegation to State#find_text with match: modes
|
|
8
|
+
- Driver#snapshot: return a State snapshot for later diff comparison
|
|
9
|
+
- match_snapshot RSpec matcher: compare current state against saved snapshot,
|
|
10
|
+
supports chars_only: true to ignore color/style changes
|
|
11
|
+
- MCP tui_diff tool: compare current terminal state against a previous snapshot,
|
|
12
|
+
returns cell-level differences
|
|
13
|
+
- MCP tui_annotate_element tool: manually register UI element annotations that
|
|
14
|
+
are picked up by tui_find_elements
|
|
15
|
+
- Drive mode: snapshot and diff commands for interactive state comparison
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Tighten tans-parser dependency to ~> 0.1.3 (includes State#diff,
|
|
20
|
+
State#annotate_role, improved dialog/statusbar detection)
|
|
21
|
+
|
|
22
|
+
## 0.2.15
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Integration with tans-parser 0.1.2: new UI element roles (:input, :label, :menu, :tab)
|
|
27
|
+
- Filter kwargs on get_by_role: text:, checked:, disabled:
|
|
28
|
+
- Singular convenience methods: button(), checkbox(), input(), label(), menu(), tab(),
|
|
29
|
+
dialog(), statusbar(), progress_bar()
|
|
30
|
+
- Element action methods: click, type(text), press_key(key)
|
|
31
|
+
- Element predicates: checked?, disabled?
|
|
32
|
+
- Element bounds accessor
|
|
33
|
+
- disabled field on Element
|
|
34
|
+
- ScopedSelector via Selector#within(element, &block) with full query API
|
|
35
|
+
- State#find_text match modes: :partial (default), :exact, :regex
|
|
36
|
+
- RSpec matchers: have_input, have_label, have_menu, have_tab, have_statusbar,
|
|
37
|
+
have_progress_bar
|
|
38
|
+
- JSON test steps: assert_input, assert_label, assert_menu, assert_tab,
|
|
39
|
+
assert_statusbar, assert_progress_bar
|
|
40
|
+
- MCP tool: tui_element_actions — returns click/type/press_key action hashes
|
|
41
|
+
- Enhanced MCP tui_find_elements: checked/disabled filters, new roles, (disabled)/
|
|
42
|
+
(focused) output
|
|
43
|
+
- Enhanced MCP tui_find_text: match: parameter for exact/regex mode
|
|
44
|
+
- CLI drive mode: elements command now shows inputs, labels, menus, tabs
|
|
45
|
+
- Updated help texts (tui-td help test, tui-td help rspec) with new steps and matchers
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- RSpec matchers (have_button, have_checkbox, have_role) now use tans-parser filter
|
|
50
|
+
kwargs instead of manual .select post-filtering
|
|
51
|
+
- JSON test runner check_role helper uses tans-parser filter kwargs
|
|
52
|
+
- have_checkbox now supports .unchecked chain
|
|
53
|
+
|
|
54
|
+
### Removed
|
|
55
|
+
|
|
56
|
+
- Custom coordinate-based Selector#within (replaced by tans-parser's element-based
|
|
57
|
+
within with ScopedSelector)
|
|
58
|
+
|
|
59
|
+
## 0.2.14
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- Tighten tans-parser dependency from `~> 0.1` to `~> 0.1.1` to ensure the required
|
|
64
|
+
Selector/Element classes are present (0.1.0 lacks them)
|
|
65
|
+
|
|
3
66
|
## 0.2.13
|
|
4
67
|
|
|
5
68
|
### Added
|
data/README.md
CHANGED
|
@@ -286,7 +286,13 @@ tui-td test examples/echo_test.json
|
|
|
286
286
|
| `assert_button` | `"text"` | Assert a button with given text is visible (`[ OK ]`, `(Cancel)`, `<Submit>`) |
|
|
287
287
|
| `assert_dialog` | — | Assert a dialog (box-drawing region) is visible |
|
|
288
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`) |
|
|
289
|
+
| `assert_role` | `":button", "text": "OK"` | Generic role assertion (`:button`, `:checkbox`, `:dialog`, `:statusbar`, `:progress`, `:input`, `:label`, `:menu`, `:tab`) |
|
|
290
|
+
| `assert_input` | `"text"` (optional) | Assert an input field (`[____]`) is visible |
|
|
291
|
+
| `assert_label` | `"text"` | Assert a label (text ending with colon) is visible |
|
|
292
|
+
| `assert_menu` | `"text"` (optional) | Assert a menu bar or dropdown item is visible |
|
|
293
|
+
| `assert_tab` | `"text"` | Assert a tab (`[Tab1]`) is visible |
|
|
294
|
+
| `assert_statusbar` | — | Assert a status bar (bottom row with background) is visible |
|
|
295
|
+
| `assert_progress_bar` | `"text"` (optional) | Assert a progress bar (`[####]`) is visible |
|
|
290
296
|
| `close` | — | Close the TUI |
|
|
291
297
|
|
|
292
298
|
Example with `html` step for before/after snapshots:
|
|
@@ -364,7 +370,13 @@ end
|
|
|
364
370
|
| `have_button("OK")` | Assert a button with given text is visible |
|
|
365
371
|
| `have_dialog` | Assert a dialog (box-drawing region) is visible |
|
|
366
372
|
| `have_checkbox("Label").checked` | Assert a checkbox with given label (chain `.checked`) |
|
|
367
|
-
| `have_role(:button, text: "OK")` | Generic role assertion with optional text
|
|
373
|
+
| `have_role(:button, text: "OK", checked: true, disabled: false)` | Generic role assertion with optional text, checked, disabled filters |
|
|
374
|
+
| `have_input` | Assert an input field (`[____]`) is visible |
|
|
375
|
+
| `have_label("Name")` | Assert a label (text ending with colon) is visible |
|
|
376
|
+
| `have_menu` | Assert a menu bar or dropdown item is visible |
|
|
377
|
+
| `have_tab("File")` | Assert a tab is visible |
|
|
378
|
+
| `have_statusbar` | Assert a status bar (bottom row with background) is visible |
|
|
379
|
+
| `have_progress_bar("50%")` | Assert a progress bar (`[####]`) is visible |
|
|
368
380
|
| `have_exit_status(N)` | Assert the driver process exit status equals N |
|
|
369
381
|
|
|
370
382
|
## MCP Server — AI Integration
|
|
@@ -390,8 +402,11 @@ tui-td serve
|
|
|
390
402
|
| `tui_html_render` | Render terminal state as a self-contained HTML document. Returns HTML inline or saves to file. |
|
|
391
403
|
| `tui_wait_for_exit` | Wait until the TUI process exits. Returns exit status. |
|
|
392
404
|
| `tui_exit_status` | Get the exit status code (nil if still running). |
|
|
393
|
-
| `tui_find_text` | Search for text or regex in terminal state.
|
|
394
|
-
| `tui_find_elements` | Detect UI elements (buttons, checkboxes, dialogs, etc.) with optional role
|
|
405
|
+
| `tui_find_text` | Search for text or regex in terminal state. Supports match modes: `partial` (default), `exact`, `regex`. |
|
|
406
|
+
| `tui_find_elements` | Detect UI elements (buttons, checkboxes, dialogs, inputs, labels, menus, tabs, etc.) with optional role, text, checked, and disabled filters. |
|
|
407
|
+
| `tui_element_actions` | Get click/type/press_key action hashes for a detected UI element. For AI-driven interaction. |
|
|
408
|
+
| `tui_diff` | Compare current state against a previous snapshot. Returns cell-level differences. |
|
|
409
|
+
| `tui_annotate_element` | Manually register a UI element annotation. Picked up by tui_find_elements. |
|
|
395
410
|
| `tui_close` | Close the TUI and clean up. |
|
|
396
411
|
|
|
397
412
|
### MCP configuration
|
|
@@ -441,10 +456,13 @@ Add to your MCP client configuration:
|
|
|
441
456
|
// 9. Find UI elements by role
|
|
442
457
|
{"method": "tools/call", "params": {"name": "tui_find_elements", "arguments": {"role": "button"}}}
|
|
443
458
|
|
|
444
|
-
// 10.
|
|
459
|
+
// 10. Get actions for an element
|
|
460
|
+
{"method": "tools/call", "params": {"name": "tui_element_actions", "arguments": {"role": "button", "text": "OK"}}}
|
|
461
|
+
|
|
462
|
+
// 11. Check exit status (or wait for exit)
|
|
445
463
|
{"method": "tools/call", "params": {"name": "tui_exit_status", "arguments": {}}}
|
|
446
464
|
|
|
447
|
-
//
|
|
465
|
+
// 12. Clean up
|
|
448
466
|
{"method": "tools/call", "params": {"name": "tui_close", "arguments": {}}}
|
|
449
467
|
```
|
|
450
468
|
|
data/lib/tui_td/cli.rb
CHANGED
|
@@ -41,6 +41,10 @@ module TUITD
|
|
|
41
41
|
opts.separator " state Show terminal state as pretty JSON"
|
|
42
42
|
opts.separator " raw Show raw ANSI output"
|
|
43
43
|
opts.separator " elements Show detected UI elements (buttons, dialogs, etc.)"
|
|
44
|
+
opts.separator " snapshot <name> Save current state as named snapshot to disk"
|
|
45
|
+
opts.separator " snapshot Save current state in-memory (legacy)"
|
|
46
|
+
opts.separator " diff <name> Compare current state against named snapshot on disk"
|
|
47
|
+
opts.separator " diff Compare against in-memory snapshot (legacy)"
|
|
44
48
|
opts.separator " key <name> Send keystroke (enter, tab, escape, up, down, left, right,"
|
|
45
49
|
opts.separator " backspace, ctrl_c, ctrl_d)"
|
|
46
50
|
opts.separator " <text> Send text to the TUI"
|
|
@@ -202,8 +206,53 @@ module TUITD
|
|
|
202
206
|
puts "Buttons: #{selector.buttons.map { |e| "#{e.text}@[#{e.row},#{e.col}]" }.join(", ")}"
|
|
203
207
|
puts "Checkboxes: #{selector.checkboxes.map { |e| "#{e.text} (#{e.checked ? "✓" : "☐"})" }.join(", ")}"
|
|
204
208
|
puts "Dialogs: #{selector.dialogs.map { |e| "\"#{e.text}\" #{e.width}x#{e.height}" }.join(", ")}"
|
|
209
|
+
puts "Inputs: #{selector.inputs.map { |e| "[__]@[#{e.row},#{e.col}]" }.join(", ")}"
|
|
210
|
+
puts "Labels: #{selector.labels.map(&:text).join(", ")}"
|
|
211
|
+
puts "Menus: #{selector.menus.map(&:text).join(", ")}"
|
|
212
|
+
puts "Tabs: #{selector.tabs.map { |e| "#{e.text}#{" (focused)" if e.focused}" }.join(", ")}"
|
|
205
213
|
puts "Statusbars: #{selector.statusbars.map(&:text).join(", ")}"
|
|
206
214
|
puts "Progress: #{selector.progress_bars.map(&:text).join(", ")}"
|
|
215
|
+
elsif input.start_with?("snapshot ")
|
|
216
|
+
name = input.split(" ", 2).last.strip
|
|
217
|
+
unless name.empty?
|
|
218
|
+
snap = Snapshot.new(name)
|
|
219
|
+
snap.save(driver.state_data)
|
|
220
|
+
puts "Snapshot '#{name}' saved to #{snap.path}."
|
|
221
|
+
end
|
|
222
|
+
elsif input == "snapshot"
|
|
223
|
+
@last_snapshot = State.new(driver.state_data)
|
|
224
|
+
puts "In-memory snapshot saved."
|
|
225
|
+
elsif input.start_with?("diff ")
|
|
226
|
+
name = input.split(" ", 2).last.strip
|
|
227
|
+
unless name.empty?
|
|
228
|
+
snap = Snapshot.new(name)
|
|
229
|
+
unless snap.exists?
|
|
230
|
+
puts "No snapshot '#{name}' found at #{snap.path}."
|
|
231
|
+
next
|
|
232
|
+
end
|
|
233
|
+
result = snap.compare(driver.state_data)
|
|
234
|
+
if result.passed?
|
|
235
|
+
puts "No differences. Snapshot '#{name}' matches."
|
|
236
|
+
else
|
|
237
|
+
puts result.message
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
elsif input == "diff"
|
|
241
|
+
if @last_snapshot
|
|
242
|
+
current = State.new(driver.state_data)
|
|
243
|
+
diffs = current.diff(@last_snapshot)
|
|
244
|
+
if diffs.empty?
|
|
245
|
+
puts "No differences."
|
|
246
|
+
else
|
|
247
|
+
puts "#{diffs.size} difference(s):"
|
|
248
|
+
diffs.first(10).each do |d|
|
|
249
|
+
puts " [#{d[:row]},#{d[:col]}] #{d[:before][:char].inspect} -> #{d[:after][:char].inspect}"
|
|
250
|
+
end
|
|
251
|
+
puts " ..." if diffs.size > 10
|
|
252
|
+
end
|
|
253
|
+
else
|
|
254
|
+
puts "No snapshot saved. Use 'snapshot <name>' or 'snapshot' first."
|
|
255
|
+
end
|
|
207
256
|
elsif input.start_with?("key ")
|
|
208
257
|
driver.send_keys(input.split(" ", 2).last.to_sym)
|
|
209
258
|
else
|
|
@@ -433,7 +482,27 @@ module TUITD
|
|
|
433
482
|
|
|
434
483
|
{"assert_role": ":button", "text": "OK"}
|
|
435
484
|
Generic role assertion. Accepts :button, :checkbox, :dialog,
|
|
436
|
-
:statusbar, :progress
|
|
485
|
+
:statusbar, :progress, :input, :label, :menu, :tab.
|
|
486
|
+
Optional "text", "checked", and "disabled" filters.
|
|
487
|
+
|
|
488
|
+
{"assert_input": true} or {"assert_input": "text"}
|
|
489
|
+
Assert that an input field ([____]) is visible. Optional text
|
|
490
|
+
filter to match adjacent label.
|
|
491
|
+
|
|
492
|
+
{"assert_label": "Name"}
|
|
493
|
+
Assert that a label (text ending with colon) is visible.
|
|
494
|
+
|
|
495
|
+
{"assert_menu": true} or {"assert_menu": "File | Edit"}
|
|
496
|
+
Assert that a menu bar or dropdown item is visible.
|
|
497
|
+
|
|
498
|
+
{"assert_tab": "File"}
|
|
499
|
+
Assert that a tab ([Tab1]) is visible.
|
|
500
|
+
|
|
501
|
+
{"assert_statusbar": true}
|
|
502
|
+
Assert that a status bar (bottom row with background) is visible.
|
|
503
|
+
|
|
504
|
+
{"assert_progress_bar": true} or {"assert_progress_bar": "50%"}
|
|
505
|
+
Assert that a progress bar ([#### ]) is visible.
|
|
437
506
|
|
|
438
507
|
Example test file: examples/echo_test.json
|
|
439
508
|
HELP
|
|
@@ -518,9 +587,41 @@ module TUITD
|
|
|
518
587
|
|
|
519
588
|
have_role(:button, text: "OK")
|
|
520
589
|
Generic role matcher. Accepts :button, :checkbox, :dialog,
|
|
521
|
-
:statusbar, :progress
|
|
590
|
+
:statusbar, :progress, :input, :label, :menu, :tab.
|
|
591
|
+
Optional text:, checked:, disabled: filters.
|
|
522
592
|
Usage: expect(state).to have_role(:statusbar)
|
|
523
593
|
|
|
594
|
+
have_input
|
|
595
|
+
Passes if an input field ([____]) is visible.
|
|
596
|
+
Usage: expect(state).to have_input
|
|
597
|
+
Usage: expect(state).to have_input("Name")
|
|
598
|
+
|
|
599
|
+
have_label("Name")
|
|
600
|
+
Passes if a label (text ending with colon) is visible.
|
|
601
|
+
Usage: expect(state).to have_label("Name")
|
|
602
|
+
|
|
603
|
+
have_menu
|
|
604
|
+
Passes if a menu bar or dropdown item is visible.
|
|
605
|
+
Usage: expect(state).to have_menu
|
|
606
|
+
|
|
607
|
+
have_tab("File")
|
|
608
|
+
Passes if a tab is visible.
|
|
609
|
+
Usage: expect(state).to have_tab("File")
|
|
610
|
+
|
|
611
|
+
have_statusbar
|
|
612
|
+
Passes if a status bar (bottom row with background) is visible.
|
|
613
|
+
Usage: expect(state).to have_statusbar
|
|
614
|
+
|
|
615
|
+
have_progress_bar
|
|
616
|
+
Passes if a progress bar ([#### ]) is visible.
|
|
617
|
+
Usage: expect(state).to have_progress_bar
|
|
618
|
+
|
|
619
|
+
match_snapshot(snapshot, chars_only: false)
|
|
620
|
+
Passes if the current state matches a previously saved snapshot.
|
|
621
|
+
Use chars_only: true to ignore color/style changes.
|
|
622
|
+
Usage: pre = driver.snapshot; ... ; expect(driver).to match_snapshot(pre)
|
|
623
|
+
Usage: expect(state).to match_snapshot(snap, chars_only: true)
|
|
624
|
+
|
|
524
625
|
Driver matchers (work on TUITD::Driver, not State)
|
|
525
626
|
--------------------------------------------------
|
|
526
627
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TUITD
|
|
4
|
+
# Global configuration for tui-td.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# TUITD.configure do |c|
|
|
8
|
+
# c.snapshot_dir = "spec/snapshots"
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
class Configuration
|
|
12
|
+
attr_accessor :snapshot_dir
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@snapshot_dir = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Check if UPDATE_SNAPSHOTS env var is set to update mode.
|
|
19
|
+
def update_snapshots?
|
|
20
|
+
%w[1 true].include?(ENV["UPDATE_SNAPSHOTS"].to_s)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def configuration
|
|
26
|
+
@configuration ||= Configuration.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def configure
|
|
30
|
+
yield configuration
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/tui_td/driver.rb
CHANGED
|
@@ -211,6 +211,19 @@ module TUITD
|
|
|
211
211
|
Screenshot.new(@state).render(output_path)
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
+
# Search for text or regex pattern in the current terminal state.
|
|
215
|
+
# Delegates to TansParser::State#find_text.
|
|
216
|
+
# Supports match modes: :partial (default, substring), :exact, :regex.
|
|
217
|
+
def find_text(pattern, match: :partial)
|
|
218
|
+
TUITD::State.new(state_data).find_text(pattern, match: match)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Return a snapshot of the current terminal state as a TUITD::State object.
|
|
222
|
+
# Can be compared later with match_snapshot or State#diff.
|
|
223
|
+
def snapshot
|
|
224
|
+
TUITD::State.new(state_data)
|
|
225
|
+
end
|
|
226
|
+
|
|
214
227
|
# Close the driver and clean up
|
|
215
228
|
def close
|
|
216
229
|
_stop_reader_thread
|
data/lib/tui_td/matchers.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/ModuleLength, Metrics/BlockLength, Metrics/ParameterLists, Layout/LineLength
|
|
4
|
+
|
|
3
5
|
require "rspec/expectations"
|
|
4
6
|
|
|
5
7
|
# RSpec matchers for TUITD::State and TUITD::Driver objects.
|
|
@@ -137,7 +139,7 @@ module TUITD
|
|
|
137
139
|
RSpec::Matchers.define :have_button do |expected|
|
|
138
140
|
match do |actual|
|
|
139
141
|
Matchers.auto_wait(actual) do |s|
|
|
140
|
-
Selector.new(s).
|
|
142
|
+
Selector.new(s).button(text: expected)
|
|
141
143
|
end
|
|
142
144
|
end
|
|
143
145
|
|
|
@@ -158,50 +160,59 @@ module TUITD
|
|
|
158
160
|
|
|
159
161
|
RSpec::Matchers.define :have_checkbox do |expected|
|
|
160
162
|
chain(:checked) { @checked = true }
|
|
163
|
+
chain(:unchecked) { @checked = false }
|
|
161
164
|
|
|
162
165
|
match do |actual|
|
|
163
166
|
Matchers.auto_wait(actual) do |s|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
found.any?
|
|
167
|
+
filters = { text: expected }
|
|
168
|
+
filters[:checked] = @checked unless @checked.nil?
|
|
169
|
+
Selector.new(s).checkbox(**filters)
|
|
168
170
|
end
|
|
169
171
|
end
|
|
170
172
|
|
|
171
173
|
description do
|
|
172
174
|
desc = "have checkbox #{expected.inspect}"
|
|
173
|
-
desc += " (checked)" if @checked
|
|
175
|
+
desc += " (checked)" if @checked == true
|
|
176
|
+
desc += " (unchecked)" if @checked == false
|
|
174
177
|
desc
|
|
175
178
|
end
|
|
176
179
|
failure_message do |_actual|
|
|
177
180
|
desc = "expected terminal to have checkbox #{expected.inspect}"
|
|
178
|
-
desc += " (checked)" if @checked
|
|
181
|
+
desc += " (checked)" if @checked == true
|
|
182
|
+
desc += " (unchecked)" if @checked == false
|
|
179
183
|
desc
|
|
180
184
|
end
|
|
181
185
|
failure_message_when_negated do |_actual|
|
|
182
186
|
desc = "expected terminal NOT to have checkbox #{expected.inspect}"
|
|
183
|
-
desc += " (checked)" if @checked
|
|
187
|
+
desc += " (checked)" if @checked == true
|
|
188
|
+
desc += " (unchecked)" if @checked == false
|
|
184
189
|
desc
|
|
185
190
|
end
|
|
186
191
|
end
|
|
187
192
|
|
|
188
|
-
RSpec::Matchers.define :have_role do |role, text: nil|
|
|
193
|
+
RSpec::Matchers.define :have_role do |role, text: nil, checked: nil, disabled: nil|
|
|
189
194
|
match do |actual|
|
|
190
195
|
Matchers.auto_wait(actual) do |s|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
filters = {}
|
|
197
|
+
filters[:text] = text if text
|
|
198
|
+
filters[:checked] = checked unless checked.nil?
|
|
199
|
+
filters[:disabled] = disabled unless disabled.nil?
|
|
200
|
+
Selector.new(s).get_by_role(role, **filters).any?
|
|
194
201
|
end
|
|
195
202
|
end
|
|
196
203
|
|
|
197
204
|
description do
|
|
198
205
|
desc = "have role :#{role}"
|
|
199
206
|
desc += " with text #{text.inspect}" if text
|
|
207
|
+
desc += " (checked)" if checked == true
|
|
208
|
+
desc += " (disabled)" if disabled == true
|
|
200
209
|
desc
|
|
201
210
|
end
|
|
202
211
|
failure_message do |_actual|
|
|
203
212
|
desc = "expected terminal to have a :#{role}"
|
|
204
213
|
desc += " with text #{text.inspect}" if text
|
|
214
|
+
desc += " (checked)" if checked == true
|
|
215
|
+
desc += " (disabled)" if disabled == true
|
|
205
216
|
desc
|
|
206
217
|
end
|
|
207
218
|
failure_message_when_negated do |_actual|
|
|
@@ -210,5 +221,240 @@ module TUITD
|
|
|
210
221
|
desc
|
|
211
222
|
end
|
|
212
223
|
end
|
|
224
|
+
|
|
225
|
+
# New role matchers (tans-parser 0.1.2)
|
|
226
|
+
|
|
227
|
+
RSpec::Matchers.define :have_input do |expected = nil|
|
|
228
|
+
match do |actual|
|
|
229
|
+
Matchers.auto_wait(actual) do |s|
|
|
230
|
+
if expected
|
|
231
|
+
Selector.new(s).input(text: expected)
|
|
232
|
+
else
|
|
233
|
+
Selector.new(s).inputs.any?
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
description do
|
|
239
|
+
expected ? "have input #{expected.inspect}" : "have an input field"
|
|
240
|
+
end
|
|
241
|
+
failure_message do |_actual|
|
|
242
|
+
expected ? "expected terminal to have an input #{expected.inspect}" : "expected terminal to have an input field"
|
|
243
|
+
end
|
|
244
|
+
failure_message_when_negated do |_actual|
|
|
245
|
+
expected ? "expected terminal NOT to have an input #{expected.inspect}" : "expected terminal NOT to have an input field"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
RSpec::Matchers.define :have_label do |expected = nil|
|
|
250
|
+
match do |actual|
|
|
251
|
+
Matchers.auto_wait(actual) do |s|
|
|
252
|
+
if expected
|
|
253
|
+
Selector.new(s).label(text: expected)
|
|
254
|
+
else
|
|
255
|
+
Selector.new(s).labels.any?
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
description do
|
|
261
|
+
expected ? "have label #{expected.inspect}" : "have a label"
|
|
262
|
+
end
|
|
263
|
+
failure_message do |_actual|
|
|
264
|
+
expected ? "expected terminal to have a label #{expected.inspect}" : "expected terminal to have a label"
|
|
265
|
+
end
|
|
266
|
+
failure_message_when_negated do |_actual|
|
|
267
|
+
expected ? "expected terminal NOT to have a label #{expected.inspect}" : "expected terminal NOT to have a label"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
RSpec::Matchers.define :have_menu do |expected = nil|
|
|
272
|
+
match do |actual|
|
|
273
|
+
Matchers.auto_wait(actual) do |s|
|
|
274
|
+
if expected
|
|
275
|
+
Selector.new(s).menu(text: expected)
|
|
276
|
+
else
|
|
277
|
+
Selector.new(s).menus.any?
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
description do
|
|
283
|
+
expected ? "have menu #{expected.inspect}" : "have a menu"
|
|
284
|
+
end
|
|
285
|
+
failure_message do |_actual|
|
|
286
|
+
expected ? "expected terminal to have a menu #{expected.inspect}" : "expected terminal to have a menu"
|
|
287
|
+
end
|
|
288
|
+
failure_message_when_negated do |_actual|
|
|
289
|
+
expected ? "expected terminal NOT to have a menu #{expected.inspect}" : "expected terminal NOT to have a menu"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
RSpec::Matchers.define :have_tab do |expected = nil|
|
|
294
|
+
match do |actual|
|
|
295
|
+
Matchers.auto_wait(actual) do |s|
|
|
296
|
+
if expected
|
|
297
|
+
Selector.new(s).tab(text: expected)
|
|
298
|
+
else
|
|
299
|
+
Selector.new(s).tabs.any?
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
description do
|
|
305
|
+
expected ? "have tab #{expected.inspect}" : "have a tab"
|
|
306
|
+
end
|
|
307
|
+
failure_message do |_actual|
|
|
308
|
+
expected ? "expected terminal to have a tab #{expected.inspect}" : "expected terminal to have a tab"
|
|
309
|
+
end
|
|
310
|
+
failure_message_when_negated do |_actual|
|
|
311
|
+
expected ? "expected terminal NOT to have a tab #{expected.inspect}" : "expected terminal NOT to have a tab"
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
RSpec::Matchers.define :have_statusbar do |expected = nil|
|
|
316
|
+
match do |actual|
|
|
317
|
+
Matchers.auto_wait(actual) do |s|
|
|
318
|
+
if expected
|
|
319
|
+
Selector.new(s).statusbar(text: expected)
|
|
320
|
+
else
|
|
321
|
+
Selector.new(s).statusbars.any?
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
description do
|
|
327
|
+
expected ? "have status bar #{expected.inspect}" : "have a status bar"
|
|
328
|
+
end
|
|
329
|
+
failure_message do |_actual|
|
|
330
|
+
expected ? "expected terminal to have a status bar #{expected.inspect}" : "expected terminal to have a status bar"
|
|
331
|
+
end
|
|
332
|
+
failure_message_when_negated do |_actual|
|
|
333
|
+
expected ? "expected terminal NOT to have a status bar #{expected.inspect}" : "expected terminal NOT to have a status bar"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
RSpec::Matchers.define :have_progress_bar do |expected = nil|
|
|
338
|
+
match do |actual|
|
|
339
|
+
Matchers.auto_wait(actual) do |s|
|
|
340
|
+
if expected
|
|
341
|
+
Selector.new(s).progress_bar(text: expected)
|
|
342
|
+
else
|
|
343
|
+
Selector.new(s).progress_bars.any?
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
description do
|
|
349
|
+
expected ? "have progress bar #{expected.inspect}" : "have a progress bar"
|
|
350
|
+
end
|
|
351
|
+
failure_message do |_actual|
|
|
352
|
+
expected ? "expected terminal to have a progress bar #{expected.inspect}" : "expected terminal to have a progress bar"
|
|
353
|
+
end
|
|
354
|
+
failure_message_when_negated do |_actual|
|
|
355
|
+
expected ? "expected terminal NOT to have a progress bar #{expected.inspect}" : "expected terminal NOT to have a progress bar"
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Snapshot comparison matcher — works with both State and Driver (auto-wait).
|
|
360
|
+
# Snapshot comparison matcher — supports both named (disk-based) and
|
|
361
|
+
# legacy in-memory State objects.
|
|
362
|
+
#
|
|
363
|
+
# Named snapshots (recommended):
|
|
364
|
+
# expect(driver).to match_snapshot("login_screen")
|
|
365
|
+
# expect(driver).to match_snapshot("login", type: :all, wait: true)
|
|
366
|
+
#
|
|
367
|
+
# First run creates the snapshot, subsequent runs compare.
|
|
368
|
+
# UPDATE_SNAPSHOTS=1 overwrites all snapshots.
|
|
369
|
+
#
|
|
370
|
+
# Legacy (backward compatible):
|
|
371
|
+
# pre = driver.snapshot
|
|
372
|
+
# expect(driver).to match_snapshot(pre, chars_only: true)
|
|
373
|
+
#
|
|
374
|
+
RSpec::Matchers.define :match_snapshot do |ref, type: nil, wait: false, chars_only: nil, ignore_rows: nil, region: nil|
|
|
375
|
+
match do |actual|
|
|
376
|
+
# Normalize type: backward compat for chars_only parameter
|
|
377
|
+
effective_type = type
|
|
378
|
+
if effective_type.nil? && !chars_only.nil?
|
|
379
|
+
effective_type = chars_only ? :text : :full
|
|
380
|
+
end
|
|
381
|
+
effective_type ||= :text
|
|
382
|
+
|
|
383
|
+
@snapshot_name = nil
|
|
384
|
+
@diff_result = nil
|
|
385
|
+
|
|
386
|
+
# Legacy path: snapshot_ref is a State object (responds to diff)
|
|
387
|
+
if ref.respond_to?(:diff)
|
|
388
|
+
Matchers.auto_wait(actual) do |s|
|
|
389
|
+
chars = effective_type == :text
|
|
390
|
+
diffs = ref.diff(s, chars_only: chars)
|
|
391
|
+
diffs.select! { |d| Array(region).include?(d[:row]) } if region
|
|
392
|
+
diffs.reject! { |d| Array(ignore_rows).include?(d[:row]) } if ignore_rows
|
|
393
|
+
@diff_result = diffs
|
|
394
|
+
@diff_result.empty?
|
|
395
|
+
end
|
|
396
|
+
else
|
|
397
|
+
# Named snapshot path
|
|
398
|
+
@snapshot_name = ref.to_s
|
|
399
|
+
snap = Snapshot.new(@snapshot_name, type: effective_type)
|
|
400
|
+
|
|
401
|
+
# Get state_data from actual
|
|
402
|
+
if wait && actual.respond_to?(:wait_for_stable)
|
|
403
|
+
begin
|
|
404
|
+
actual.wait_for_stable
|
|
405
|
+
rescue StandardError
|
|
406
|
+
nil
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
state_data = if actual.respond_to?(:state_data)
|
|
410
|
+
actual.state_data
|
|
411
|
+
elsif actual.respond_to?(:to_h)
|
|
412
|
+
actual.to_h
|
|
413
|
+
else
|
|
414
|
+
actual
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
if TUITD.configuration.update_snapshots?
|
|
418
|
+
snap.save(state_data)
|
|
419
|
+
true
|
|
420
|
+
elsif !snap.exists?
|
|
421
|
+
snap.save(state_data)
|
|
422
|
+
@diff_result = TUITD::Snapshot::ComparisonResult.new(passed: true, diff_count: 0, type: effective_type,
|
|
423
|
+
message: "Snapshot '#{@snapshot_name}' created (#{effective_type})",)
|
|
424
|
+
true
|
|
425
|
+
else
|
|
426
|
+
@diff_result = snap.compare(state_data, ignore_rows: ignore_rows, region: region)
|
|
427
|
+
@diff_result.passed?
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
description do
|
|
433
|
+
if @snapshot_name
|
|
434
|
+
"match snapshot #{@snapshot_name.inspect} (type: #{effective_type})"
|
|
435
|
+
else
|
|
436
|
+
desc = "match snapshot"
|
|
437
|
+
desc << " (chars only)" if effective_type == :text
|
|
438
|
+
desc
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
failure_message do |_actual|
|
|
443
|
+
if @snapshot_name
|
|
444
|
+
"Snapshot #{@snapshot_name.inspect} does not match.\n#{@diff_result&.message}"
|
|
445
|
+
else
|
|
446
|
+
"expected terminal to match snapshot"
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
failure_message_when_negated do |_actual|
|
|
451
|
+
if @snapshot_name
|
|
452
|
+
"expected snapshot #{@snapshot_name.inspect} NOT to match, but it did."
|
|
453
|
+
else
|
|
454
|
+
"expected terminal NOT to match snapshot, but it did."
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
213
458
|
end
|
|
214
459
|
end
|
|
460
|
+
# rubocop:enable Metrics/ModuleLength, Metrics/BlockLength, Metrics/ParameterLists, Layout/LineLength
|