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_snapshot for plain text and assert_rich_snapshot for styled ANSI output. Both 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_snapshot("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 201 def assert_rich_snapshot(name, msg = nil) caller_path = caller_locations(1, 1).first.path snapshot_dir = File.join(File.dirname(caller_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!) matches a stored ANSI snapshot.
Generates/Compares against a file with .ansi extension. You can cat this file to see exactly what the screen looked like.
assert_rich_snapshot("login_screen") # 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 131 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 86 def assert_snapshot(name, msg = nil, &) # Get the path of the test file calling this method caller_path = caller_locations(1, 1).first.path snapshot_dir = File.join(File.dirname(caller_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 snapshot.
This method simplifies snapshot testing by automatically resolving the snapshot path relative to the test file calling this method. It assumes a “snapshots” directory exists in the same directory as the test file.
# In test/test_login.rb assert_snapshot("login_screen") # Look for: test/snapshots/login_screen.txt # With normalization block assert_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.