module RatatuiRuby::TestHelper::Snapshot
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:
test/examples/my_app/test_app.rb test/examples/my_app/snapshots/initial_render.txt test/examples/my_app/snapshots/initial_render.ansi
Creating and Updating Snapshots
Run tests with UPDATE_SNAPSHOTS=1 to create or refresh snapshots:
UPDATE_SNAPSHOTS=1 bundle exec rake test
Seeding Random Data
Random data (scatter plots, generated content) breaks snapshot stability. Use a seeded Random instance instead of Kernel.rand:
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
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:
assert_snapshots("dashboard") do |lines| lines.map { |l| l.gsub(/\d{4}-\d{2}-\d{2}/, "YYYY-MM-DD") } end
Public Instance Methods
Source
# File lib/ratatui_ruby/test_helper/snapshot.rb, line 131 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
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.
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
- name
-
String name of the snapshot (without extension).
- msg
-
String optional failure message.
Source
# File lib/ratatui_ruby/test_helper/snapshot.rb, line 266 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 if block_given? lines = actual_content.split("\n") # Yield lines to user block for modification (e.g. masking IDs/Times) lines = yield(lines) actual_content = "#{lines.join("\n")}\n" end 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") # Flush filesystem buffers to ensure durability File.open(snapshot_path, "r", &:fsync) if File.exist?(snapshot_path) rescue => e warn "Failed to write rich snapshot #{snapshot_path}: #{e.message}" 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
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.
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
- name
-
String snapshot name.
- msg
-
String optional failure message.
Source
# File lib/ratatui_ruby/test_helper/snapshot.rb, line 183 def assert_screen_matches(expected, msg = nil) actual_lines = 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") # Flush filesystem buffers to ensure durability File.open(snapshot_path, "r", &:fsync) if File.exist?(snapshot_path) rescue => e warn "Failed to write snapshot #{snapshot_path}: #{e.message}" 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
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
# 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
- 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.
Source
# File lib/ratatui_ruby/test_helper/snapshot.rb, line 347 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
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.
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
- name
-
String snapshot name (without extension).
- msg
-
String optional failure message.
Source
# File lib/ratatui_ruby/test_helper/snapshot.rb, line 381 def render_rich_buffer _render_buffer_with_ansi end
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
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