Module: RatatuiRuby::TestHelper::Snapshot
- Included in:
- RatatuiRuby::TestHelper
- Defined in:
- lib/ratatui_ruby/test_helper/snapshot.rb
Overview
Snapshot testing assertions for terminal UIs.
Verifying every character of a TUI screen by hand is tedious. Snapshots let you capture the screen once and compare against it in future runs.
This mixin provides assert_plain_snapshot for plain text, assert_rich_snapshot for styled ANSI output, and assert_snapshots (plural) for both. All auto-create snapshot files on first run.
Use it to verify complex layouts, styles, and interactions without manual assertions.
Snapshot Files
Snapshots live in a snapshots/ subdirectory next to your test file:
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
test/examples/my_app/test_app.rb
test/examples/my_app/snapshots/initial_render.txt
test/examples/my_app/snapshots/initial_render.ansi
– SPDX-SnippetEnd ++
Creating and Updating Snapshots
Run tests with UPDATE_SNAPSHOTS=1 to create or refresh snapshots:
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
UPDATE_SNAPSHOTS=1 bundle exec rake test
– SPDX-SnippetEnd ++
Seeding Random Data
Random data (scatter plots, generated content) breaks snapshot stability. Use a seeded Random instance instead of Kernel.rand:
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
class MyApp
def initialize(seed: nil)
@rng = seed ? Random.new(seed) : Random.new
end
def generate_data
(0..20).map { @rng.rand(0.0..10.0) }
end
end
# In your test
def setup
@app = MyApp.new(seed: 42)
end
– SPDX-SnippetEnd ++ For libraries like Faker, see their docs on deterministic random: github.com/faker-ruby/faker#deterministic-random
Normalization Blocks
Mask dynamic content (timestamps, IDs) with a normalization block:
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
assert_snapshots("dashboard") do |lines|
lines.map { |l| l.gsub(/\d{4}-\d{2}-\d{2}/, "YYYY-MM-DD") }
end
– SPDX-SnippetEnd ++
Class-Wide Normalization
When every test in a class faces the same dynamic content, override normalize_snapshots instead of repeating the block. Return a callable (or Array of callables) that transforms lines. The hook runs before any per-call block, so the two compose naturally. See normalize_snapshots for details.
Instance Method Summary collapse
-
#assert_plain_snapshot(name, msg = nil, snapshot_dir: nil) ⇒ Object
Asserts that the current screen content matches a stored plain text snapshot.
-
#assert_rich_snapshot(name, msg = nil, snapshot_dir: nil) ⇒ Object
Asserts that the current screen content (including colors and styles) matches a stored ANSI snapshot.
-
#assert_screen_matches(expected, msg = nil) ⇒ Object
Asserts that the current screen content matches the expected content.
-
#assert_snapshots(name, msg = nil) ⇒ Object
Asserts both plain text and rich (ANSI-styled) snapshots match.
-
#render_rich_buffer ⇒ Object
Returns the current buffer content as an ANSI-encoded string.
Instance Method Details
#assert_plain_snapshot(name, msg = nil, snapshot_dir: nil) ⇒ Object
Asserts that the current screen content matches a stored plain text snapshot.
Plain text snapshots capture layout but miss styling bugs: wrong colors, missing bold, invisible text on a matching background. *Prefer assert_snapshots* (plural) to catch styling regressions.
Plain text snapshots are human-readable when viewed in any editor or diff tool. They pair well with rich snapshots for documentation. Use assert_snapshots to generate both.
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
assert_plain_snapshot("login_screen")
# Compares against: test/snapshots/login_screen.txt
# With normalization block
assert_plain_snapshot("clock") do |actual|
actual.map { |l| l.gsub(/\d{2}:\d{2}/, "XX:XX") }
end
– SPDX-SnippetEnd ++
- name
-
String name of the snapshot (without extension).
- msg
-
String optional failure message.
186 187 188 189 190 191 192 |
# File 'lib/ratatui_ruby/test_helper/snapshot.rb', line 186 def assert_plain_snapshot(name, msg = nil, snapshot_dir: nil, &) # Get the path of the test file calling this method snapshot_dir ||= File.join(File.dirname(caller_locations(1, 1).first.path), "snapshots") snapshot_path = File.join(snapshot_dir, "#{name}.txt") assert_screen_matches(snapshot_path, msg, &) end |
#assert_rich_snapshot(name, msg = nil, snapshot_dir: nil) ⇒ Object
Asserts that the current screen content (including colors and styles) matches a stored ANSI snapshot.
TUIs communicate meaning through colors and styles. Rich snapshots capture everything: wrong colors, missing bold, invisible text on a matching background. *Prefer assert_snapshots* (plural) to also generate human-readable plain text files for documentation.
The .ansi snapshot files contain ANSI escape codes. You can cat them in a terminal to see exactly what the screen looked like.
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
assert_rich_snapshot("login_screen")
# Compares against: test/snapshots/login_screen.ansi
# With normalization
assert_rich_snapshot("log_view") do |lines|
lines.map { |l| l.gsub(/\d{2}:\d{2}:\d{2}/, "HH:MM:SS") }
end
– SPDX-SnippetEnd ++
- name
-
String snapshot name.
- msg
-
String optional failure message.
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/ratatui_ruby/test_helper/snapshot.rb', line 318 def assert_rich_snapshot(name, msg = nil, snapshot_dir: nil) snapshot_dir ||= File.join(File.dirname(caller_locations(1, 1).first.path), "snapshots") snapshot_path = File.join(snapshot_dir, "#{name}.ansi") actual_content = _render_buffer_with_ansi lines = normalize_snapshots(actual_content.split("\n")) if block_given? lines = yield(lines) end actual_content = "#{lines.join("\n")}\n" update_snapshots = ENV["UPDATE_SNAPSHOTS"] == "1" || ENV["UPDATE_SNAPSHOTS"] == "true" if !File.exist?(snapshot_path) || update_snapshots FileUtils.mkdir_p(File.dirname(snapshot_path)) begin # Delete old file first to avoid git index stale-read issues FileUtils.rm_f(snapshot_path) # Write with explicit mode to ensure clean write File.write(snapshot_path, actual_content, mode: "w") rescue => e warn "Failed to write rich snapshot #{snapshot_path}: #{e.}" raise end puts (update_snapshots ? "Updated" : "Created") + " rich snapshot: #{snapshot_path}" end expected_content = File.read(snapshot_path) # Compare byte-for-byte first if expected_content != actual_content # Fallback to line-by-line diff for better error messages expected_lines = expected_content.split("\n") actual_lines = actual_content.split("\n") assert_equal expected_lines.size, actual_lines.size, "#{msg}: Line count mismatch" expected_lines.each_with_index do |exp, i| act = actual_lines[i] assert_equal exp, act, "#{msg}: Rich content mismatch at line #{i + 1}" end end end |
#assert_screen_matches(expected, msg = nil) ⇒ Object
Asserts that the current screen content matches the expected content.
Users need to verify that the entire TUI screen looks exactly as expected. Manually checking every cell or line is tedious and error-prone.
This helper compares the current buffer content against an expected string (file path) or array of strings. It supports automatic snapshot creation and updating via the UPDATE_SNAPSHOTS environment variable.
Use it to verify complex UI states, layouts, and renderings.
Usage
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
# Direct comparison
assert_screen_matches(["Line 1", "Line 2"])
# File comparison
assert_screen_matches("test/snapshots/login.txt")
# With normalization (e.g., masking dynamic data)
assert_screen_matches("test/snapshots/dashboard.txt") do |lines|
lines.map { |l| l.gsub(/User ID: \d+/, "User ID: XXX") }
end
– SPDX-SnippetEnd ++
- expected
-
String (file path) or Array<String> (content).
- msg
-
String optional failure message.
Non-Determinism
To prevent flaky tests, this assertion performs a “Flakiness Check” when creating or updating snapshots. It captures the screen content, immediately re-renders the buffer, and compares the two results.
Ensure your render logic is deterministic by seeding random number generators and stubbing time where necessary.
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/ratatui_ruby/test_helper/snapshot.rb', line 238 def assert_screen_matches(expected, msg = nil) actual_lines = normalize_snapshots(buffer_content) if block_given? actual_lines = yield(actual_lines) end if expected.is_a?(String) # Snapshot file mode snapshot_path = expected update_snapshots = ENV["UPDATE_SNAPSHOTS"] == "1" || ENV["UPDATE_SNAPSHOTS"] == "true" if !File.exist?(snapshot_path) || update_snapshots FileUtils.mkdir_p(File.dirname(snapshot_path)) content_to_write = "#{actual_lines.join("\n")}\n" begin # Delete old file first to avoid git index stale-read issues FileUtils.rm_f(snapshot_path) # Write with explicit mode to ensure clean write File.write(snapshot_path, content_to_write, mode: "w") rescue => e warn "Failed to write snapshot #{snapshot_path}: #{e.}" raise end if update_snapshots puts "Updated snapshot: #{snapshot_path}" else puts "Created snapshot: #{snapshot_path}" end end expected_lines = File.readlines(snapshot_path, chomp: true) else # Direct comparison mode expected_lines = expected end msg ||= "Screen content mismatch" assert_equal expected_lines.size, actual_lines.size, "#{msg}: Line count mismatch" expected_lines.each_with_index do |expected_line, i| actual_line = actual_lines[i] assert_equal expected_line, actual_line, "#{msg}: Line #{i + 1} mismatch.\nExpected: #{expected_line.inspect}\nActual: #{actual_line.inspect}" end end |
#assert_snapshots(name, msg = nil) ⇒ Object
Asserts both plain text and rich (ANSI-styled) snapshots match.
This is the recommended snapshot assertion. It calls both assert_plain_snapshot and assert_rich_snapshot with the same name, generating .txt and .ansi files.
Rich snapshots catch styling bugs that plain text misses. Plain text snapshots are human-readable in any editor or diff tool, making them valuable for documentation and code review. Together, they provide comprehensive coverage and discoverability.
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
assert_snapshots("login_screen")
# Creates/compares: snapshots/login_screen.txt AND snapshots/login_screen.ansi
# With normalization (masks dynamic content like timestamps)
assert_snapshots("dashboard") do |lines|
lines.map { |l| l.gsub(/\d{2}:\d{2}:\d{2}/, "HH:MM:SS") }
end
– SPDX-SnippetEnd ++
- name
-
String snapshot name (without extension).
- msg
-
String optional failure message.
395 396 397 398 399 |
# File 'lib/ratatui_ruby/test_helper/snapshot.rb', line 395 def assert_snapshots(name, msg = nil, &) snapshot_dir = File.join(File.dirname(caller_locations(1, 1).first.path), "snapshots") assert_plain_snapshot(name, msg, snapshot_dir:, &) assert_rich_snapshot(name, msg, snapshot_dir:, &) end |
#render_rich_buffer ⇒ Object
Returns the current buffer content as an ANSI-encoded string.
The rich snapshot assertion captures styled output. Sometimes you need the raw ANSI string for debugging, custom assertions, or programmatic inspection.
This method renders the buffer with escape codes for colors and modifiers. You can ‘cat` the output to see exactly what the terminal would display.
Example
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
with_test_terminal(80, 25) do
RatatuiRuby.run do |tui|
tui.draw tui.paragraph(text: "Hello", block: tui.block(title: "Test"))
break
end
ansi_output = render_rich_buffer
puts ansi_output # Shows styled output with escape codes
end
– SPDX-SnippetEnd ++
429 430 431 |
# File 'lib/ratatui_ruby/test_helper/snapshot.rb', line 429 def render_rich_buffer _render_buffer_with_ansi end |