tui-td 0.2.14 → 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.
@@ -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
@@ -5,27 +5,4 @@ require "tans-parser"
5
5
  module TUITD
6
6
  Selector = TansParser::Selector
7
7
  Element = TansParser::Element
8
-
9
- # Extend the aliased Selector with within scoping.
10
- # within is a testing-specific pattern (scope queries to a dialog region)
11
- # and therefore lives in tui-td rather than tans-parser.
12
- Selector.class_eval do
13
- # Return a new Selector whose elements are filtered to the given bounding box.
14
- # Coordinates of returned elements are relative to the box origin.
15
- def within(top_row, left_col, width, height)
16
- scoped = @elements.select do |e|
17
- e.row >= top_row && e.row < top_row + height &&
18
- e.col >= left_col && e.col < left_col + width
19
- end
20
- scoped.each do |e|
21
- e = e.dup
22
- e.row -= top_row
23
- e.col -= left_col
24
- end
25
- self.class.allocate.tap do |s|
26
- s.instance_variable_set(:@state, @state)
27
- s.instance_variable_set(:@elements, scoped)
28
- end
29
- end
30
- end
31
8
  end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
4
+
5
+ require "json"
6
+ require "fileutils"
7
+
8
+ module TUITD
9
+ # Named, persisted snapshot for terminal state comparison.
10
+ #
11
+ # First run: saves the snapshot to disk (golden master).
12
+ # Subsequent runs: compares current state against the saved snapshot.
13
+ #
14
+ # Types:
15
+ # :text - chars_only comparison (ignores colors/styles), saved as JSON
16
+ # :full - full cell comparison (includes colors/styles), saved as JSON
17
+ # :png - screenshot PNG, compared byte-by-byte
18
+ # :html - HTML render, compared byte-by-byte
19
+ # :all - saves/compares all three formats
20
+ #
21
+ # Environment:
22
+ # UPDATE_SNAPSHOTS=1 — auto-update all snapshots instead of comparing
23
+ #
24
+ class Snapshot
25
+ EXTENSIONS = {
26
+ text: ".json",
27
+ full: ".json",
28
+ png: ".png",
29
+ html: ".html",
30
+ }.freeze
31
+
32
+ # Result of a snapshot comparison.
33
+ ComparisonResult = Struct.new(
34
+ :passed, :diff_count, :message, :details, :type,
35
+ keyword_init: true,
36
+ ) do
37
+ def passed?
38
+ passed
39
+ end
40
+ end
41
+
42
+ attr_reader :name, :type, :snapshot_dir
43
+
44
+ def initialize(name, type: :text, snapshot_dir: nil)
45
+ @name = name.to_s
46
+ @type = type.to_sym
47
+ @snapshot_dir = snapshot_dir || TUITD.configuration.snapshot_dir || "spec/snapshots"
48
+ FileUtils.mkdir_p(@snapshot_dir)
49
+ end
50
+
51
+ # Return the full filesystem path for the given extension.
52
+ def path(ext = EXTENSIONS.fetch(@type, ".json"))
53
+ File.join(@snapshot_dir, "#{@name}#{ext}")
54
+ end
55
+
56
+ # Check if the primary snapshot file exists on disk.
57
+ def exists?
58
+ if @type == :all
59
+ %i[text png html].all? { |t| File.exist?(path(EXTENSIONS[t])) }
60
+ else
61
+ File.exist?(path)
62
+ end
63
+ end
64
+
65
+ # Save terminal state as a named snapshot.
66
+ def save(state_data)
67
+ data = normalize(state_data)
68
+
69
+ File.write(path(".json"), JSON.pretty_generate(data)) if save_json?
70
+
71
+ Screenshot.new(data).render(path(".png")) if save_png?
72
+
73
+ return unless save_html?
74
+
75
+ HtmlRenderer.new(data).render(path(".html"))
76
+ end
77
+
78
+ # Compare current terminal state against the saved snapshot.
79
+ # Returns ComparisonResult.
80
+ def compare(state_data, ignore_rows: nil, region: nil)
81
+ if @type == :all
82
+ compare_all(state_data, ignore_rows: ignore_rows, region: region)
83
+ elsif png?
84
+ compare_png(state_data)
85
+ elsif html?
86
+ compare_html(state_data)
87
+ else
88
+ compare_json(state_data, chars_only: @type == :text, ignore_rows: ignore_rows, region: region)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def normalize(state_data)
95
+ return state_data if state_data.is_a?(Hash)
96
+
97
+ # Extract hash from State objects
98
+ if state_data.respond_to?(:grid) && state_data.respond_to?(:rows)
99
+ return {
100
+ size: { rows: state_data.rows, cols: state_data.cols },
101
+ cursor: state_data.cursor,
102
+ rows: state_data.grid,
103
+ }
104
+ end
105
+
106
+ state_data
107
+ end
108
+
109
+ def save_json?
110
+ %i[text full all].include?(@type)
111
+ end
112
+
113
+ def save_png?
114
+ %i[png all].include?(@type)
115
+ end
116
+
117
+ def save_html?
118
+ %i[html all].include?(@type)
119
+ end
120
+
121
+ def png?
122
+ @type == :png
123
+ end
124
+
125
+ def html?
126
+ @type == :html
127
+ end
128
+
129
+ def compare_json(state_data, chars_only:, ignore_rows: nil, region: nil)
130
+ file = path(".json")
131
+ return missing_file_result(file) unless File.exist?(file)
132
+
133
+ begin
134
+ saved = JSON.parse(File.read(file), symbolize_names: true)
135
+ rescue JSON::ParserError => e
136
+ return ComparisonResult.new(
137
+ passed: false, diff_count: 1, type: @type,
138
+ message: "Failed to parse snapshot JSON: #{e.message}",
139
+ )
140
+ end
141
+
142
+ current = TUITD::State.new(normalize(state_data))
143
+ saved_state = TUITD::State.new(saved)
144
+ diffs = current.diff(saved_state, chars_only: chars_only)
145
+
146
+ # Restrict to specified region first, then remove ignored rows
147
+ if region
148
+ region = Array(region)
149
+ diffs.select! { |d| region.include?(d[:row]) }
150
+ end
151
+ if ignore_rows
152
+ ignored = Array(ignore_rows)
153
+ diffs.reject! { |d| ignored.include?(d[:row]) }
154
+ end
155
+
156
+ if diffs.empty?
157
+ ComparisonResult.new(
158
+ passed: true, diff_count: 0, type: @type,
159
+ message: "Snapshot '#{@name}' matches (#{@type})",
160
+ )
161
+ else
162
+ details = diffs.first(20).map do |d|
163
+ {
164
+ row: d[:row], col: d[:col],
165
+ before: d[:before], after: d[:after],
166
+ }
167
+ end
168
+ msg = ["Snapshot '#{@name}' has #{diffs.size} difference(s) (#{@type}):"]
169
+ diffs.first(20).each do |d|
170
+ msg << " [#{d[:row]},#{d[:col]}] #{d[:before][:char].inspect} -> #{d[:after][:char].inspect}"
171
+ end
172
+ msg << " ... (truncated)" if diffs.size > 20
173
+ ComparisonResult.new(
174
+ passed: false, diff_count: diffs.size, type: @type,
175
+ message: msg.join("\n"), details: details,
176
+ )
177
+ end
178
+ end
179
+
180
+ def compare_png(state_data)
181
+ file = path(".png")
182
+ return missing_file_result(file) unless File.exist?(file)
183
+
184
+ expected = File.binread(file)
185
+ tmp = File.join(@snapshot_dir, ".#{@name}_tmp.png")
186
+ Screenshot.new(state_data).render(tmp)
187
+ actual = File.binread(tmp)
188
+ FileUtils.rm_f(tmp)
189
+
190
+ if expected == actual
191
+ ComparisonResult.new(
192
+ passed: true, diff_count: 0, type: :png,
193
+ message: "Snapshot '#{@name}' matches (png)",
194
+ )
195
+ else
196
+ ComparisonResult.new(
197
+ passed: false, diff_count: 1, type: :png,
198
+ message: "Snapshot '#{@name}' does not match (png — pixel difference)",
199
+ )
200
+ end
201
+ end
202
+
203
+ def compare_html(state_data)
204
+ file = path(".html")
205
+ return missing_file_result(file) unless File.exist?(file)
206
+
207
+ expected = File.read(file)
208
+ actual = HtmlRenderer.new(state_data).to_html
209
+
210
+ if expected == actual
211
+ ComparisonResult.new(
212
+ passed: true, diff_count: 0, type: :html,
213
+ message: "Snapshot '#{@name}' matches (html)",
214
+ )
215
+ else
216
+ ComparisonResult.new(
217
+ passed: false, diff_count: 1, type: :html,
218
+ message: "Snapshot '#{@name}' does not match (html — content difference)",
219
+ )
220
+ end
221
+ end
222
+
223
+ def compare_all(state_data, ignore_rows: nil, region: nil)
224
+ results = []
225
+ results << compare_json(state_data, chars_only: true, ignore_rows: ignore_rows, region: region)
226
+ results << compare_png(state_data)
227
+ results << compare_html(state_data)
228
+
229
+ all_passed = results.all?(&:passed?)
230
+ messages = results.map(&:message).join("\n")
231
+ total_diffs = results.sum(&:diff_count)
232
+
233
+ ComparisonResult.new(
234
+ passed: all_passed, diff_count: total_diffs, type: :all,
235
+ message: messages,
236
+ )
237
+ end
238
+
239
+ def missing_file_result(file)
240
+ ComparisonResult.new(
241
+ passed: false, diff_count: 0, type: @type,
242
+ message: "Snapshot '#{@name}' not found at #{file}",
243
+ )
244
+ end
245
+ end
246
+ end
247
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
@@ -170,11 +170,62 @@ module TUITD
170
170
  check_role(driver, :dialog, nil)
171
171
 
172
172
  when "assert_checkbox"
173
- check_role(driver, :checkbox, value.to_s, checked: step[:checked])
173
+ check_role(driver, :checkbox, value.to_s, checked: step[:checked], disabled: step[:disabled])
174
174
 
175
175
  when "assert_role"
176
176
  role = step[:role]&.to_sym
177
- check_role(driver, role, value.to_s)
177
+ check_role(driver, role, value.to_s, checked: step[:checked], disabled: step[:disabled])
178
+
179
+ when "assert_input"
180
+ check_role(driver, :input, value == true ? nil : value.to_s)
181
+
182
+ when "assert_label"
183
+ check_role(driver, :label, value == true ? nil : value.to_s)
184
+
185
+ when "assert_menu"
186
+ check_role(driver, :menu, value == true ? nil : value.to_s)
187
+
188
+ when "assert_tab"
189
+ check_role(driver, :tab, value == true ? nil : value.to_s)
190
+
191
+ when "assert_statusbar"
192
+ check_role(driver, :statusbar, value == true ? nil : value.to_s)
193
+
194
+ when "assert_progress_bar"
195
+ check_role(driver, :progress, value == true ? nil : value.to_s)
196
+
197
+ when "snapshot"
198
+ ensure_driver!(driver)
199
+ driver.wait_for_stable if step[:wait] || step["wait"]
200
+ snap_type = (step[:type] || step["type"] || :text).to_s.to_sym
201
+ snap = Snapshot.new(value, type: snap_type)
202
+ snap.save(driver.state_data)
203
+ Result.new(step: action, passed: true,
204
+ message: "Snapshot saved: #{value} (#{snap_type})",)
205
+
206
+ when "assert_snapshot"
207
+ ensure_driver!(driver)
208
+ driver.wait_for_stable if step[:wait] || step["wait"]
209
+ snap_type = (step[:type] || step["type"] || :text).to_s.to_sym
210
+ snap = Snapshot.new(value, type: snap_type)
211
+
212
+ if TUITD.configuration.update_snapshots?
213
+ snap.save(driver.state_data)
214
+ Result.new(step: action, passed: true,
215
+ message: "Snapshot updated: #{value} (#{snap_type})",)
216
+ elsif !snap.exists?
217
+ snap.save(driver.state_data)
218
+ Result.new(step: action, passed: true,
219
+ message: "Snapshot created: #{value} (#{snap_type})",)
220
+ else
221
+ result = snap.compare(driver.state_data)
222
+ msg = if result.passed?
223
+ "Snapshot matches: #{value}"
224
+ else
225
+ "Snapshot mismatch: #{value}\n#{result.message}"
226
+ end
227
+ Result.new(step: action, passed: result.passed?, message: msg)
228
+ end
178
229
 
179
230
  when "close"
180
231
  driver&.close
@@ -224,21 +275,29 @@ module TUITD
224
275
 
225
276
  private
226
277
 
227
- def check_role(driver, role, text, checked: nil)
278
+ def check_role(driver, role, text, checked: nil, disabled: nil)
228
279
  ensure_driver!(driver)
229
280
  state = State.new(driver.state_data)
230
281
  selector = Selector.new(state)
231
- elements = selector.get_by_role(role)
232
- elements = elements.select { |e| e.text&.include?(text.to_s) } if text
233
- elements = elements.select { |e| e.checked == checked } unless checked.nil?
282
+
283
+ filters = {}
284
+ filters[:text] = text.to_s if text
285
+ filters[:checked] = checked unless checked.nil?
286
+ filters[:disabled] = disabled unless disabled.nil?
287
+ elements = selector.get_by_role(role, **filters)
234
288
 
235
289
  action = "assert_#{role}"
236
290
  if elements.any?
237
291
  count = elements.size
238
292
  desc = text ? "#{role} #{text.inspect}" : role.to_s
293
+ desc += " (checked)" if checked == true
294
+ desc += " (unchecked)" if checked == false
295
+ desc += " (disabled)" if disabled == true
239
296
  Result.new(step: action, passed: true, message: "Found #{count} #{desc} element(s)")
240
297
  else
241
298
  desc = text ? "#{role} with text #{text.inspect}" : role.to_s
299
+ desc += " (checked)" if checked == true
300
+ desc += " (disabled)" if disabled == true
242
301
  Result.new(step: action, passed: false, message: "No #{desc} found")
243
302
  end
244
303
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TUITD
4
- VERSION = "0.2.14"
4
+ VERSION = "0.2.19"
5
5
  end
data/lib/tui_td.rb CHANGED
@@ -13,10 +13,13 @@ require_relative "tui_td/driver"
13
13
  require_relative "tui_td/ansi_parser"
14
14
  require_relative "tui_td/ansi_utils"
15
15
  require_relative "tui_td/state"
16
+ require_relative "tui_td/configuration"
17
+ require_relative "tui_td/snapshot"
16
18
  require_relative "tui_td/screenshot"
17
19
  require_relative "tui_td/html_renderer"
18
20
  require_relative "tui_td/test_runner"
19
21
  require_relative "tui_td/selector"
22
+ require_relative "tui_td/minitest/assertions"
20
23
  require_relative "tui_td/mcp/server"
21
24
  require_relative "tui_td/cli"
22
25