Selkie.git | xt/ | 01-snapshot.rakutest
use Test;
use lib 'lib';
use Selkie::Test::Snapshot;
use Selkie::Test::Keys;
use Selkie::Widget::Text;
use Selkie::Widget::Border;
use Selkie::Widget::ListView;
use Selkie::Layout::VBox;
use Selkie::Sizing;
use Selkie::Style;
sub remove-tree(IO::Path:D $path --> Nil) {
return unless $path.e;
if $path.d {
remove-tree($_) for $path.dir;
$path.rmdir;
}
else {
$path.unlink;
}
}
# Use a temp directory so the tests don't leave files on disk.
my $tmp-dir = $*TMPDIR.add("selkie-snap-{$*PID}").Str;
LEAVE { remove-tree($tmp-dir.IO) }
plan 9;
subtest "render-to-string of a simple Text widget" => {
plan 2;
my $text = Selkie::Widget::Text.new(
text => 'hello',
sizing => Sizing.fixed(1),
);
my $out = render-to-string($text, rows => 1, cols => 20);
isa-ok $out, Str, 'returns a string';
ok $out.starts-with('hello'), 'content rendered';
};
subtest "render-to-string trims trailing whitespace per line" => {
plan 1;
my $text = Selkie::Widget::Text.new(text => 'hi', sizing => Sizing.fixed(1));
my $out = render-to-string($text, rows => 1, cols => 40);
is $out, 'hi', 'trailing spaces stripped';
};
subtest "render-to-string drops trailing blank rows" => {
plan 1;
my $text = Selkie::Widget::Text.new(text => 'top', sizing => Sizing.fixed(3));
my $out = render-to-string($text, rows => 5, cols => 10);
# Text fills row 0 with 'top', rows 1-4 are blank. Output should be 'top'
# with trailing blanks trimmed.
is $out, 'top', 'only non-blank row kept';
};
subtest "snapshot-ok creates file on first run" => {
plan 3;
my $text = Selkie::Widget::Text.new(text => 'hello', sizing => Sizing.fixed(1));
my $name = 'first-run';
snapshot-ok $text, $name, rows => 1, cols => 10, dir => $tmp-dir;
my $path = $tmp-dir.IO.add("$name.snap");
ok $path.e, 'snapshot file created';
is $path.slurp.chomp, 'hello', 'file contains rendered output';
};
subtest "snapshot-ok passes when content matches stored snapshot" => {
plan 2;
# Seed the snapshot first
my $text = Selkie::Widget::Text.new(text => 'stable', sizing => Sizing.fixed(1));
snapshot-ok $text, 'stable', rows => 1, cols => 10, dir => $tmp-dir;
# Second assertion should pass (matching)
my $text2 = Selkie::Widget::Text.new(text => 'stable', sizing => Sizing.fixed(1));
snapshot-ok $text2, 'stable', rows => 1, cols => 10, dir => $tmp-dir;
};
subtest "styled render emits the marker and three blocks" => {
plan 4;
my $text = Selkie::Widget::Text.new(
text => 'hi',
sizing => Sizing.fixed(1),
style => Selkie::Style.new(fg => 0xFF8800, bold => True),
);
my $out = render-to-string($text, rows => 1, cols => 5, :capture-styles);
ok $out.starts-with('=== styled-snapshot v1 ==='),
'first line is the format marker';
ok $out.contains('--- glyphs ---'), 'glyphs block present';
ok $out.contains('--- styles ---'), 'styles block present';
ok $out.contains('--- legend ---'), 'legend block present';
};
subtest "styled render encodes fg/bg in legend" => {
plan 2;
my $text = Selkie::Widget::Text.new(
text => 'x',
sizing => Sizing.fixed(1),
style => Selkie::Style.new(fg => 0x123456),
);
my $out = render-to-string($text, rows => 1, cols => 3, :capture-styles);
ok $out.contains('fg=#123456'), 'fg color appears in legend';
ok $out.contains('.: (default)'), 'default key documented';
};
subtest "styled render encodes BOLD/ITALIC/UNDERLINE flags" => {
plan 3;
my @cases = (
{ flag => 'BOLD', kw => :bold },
{ flag => 'ITALIC', kw => :italic },
{ flag => 'UNDERLINE', kw => :underline },
);
for @cases -> %case {
my $style = Selkie::Style.new(fg => 0xFFFFFF, |%case<kw>);
my $text = Selkie::Widget::Text.new(text => 'x', sizing => Sizing.fixed(1), :$style);
my $out = render-to-string($text, rows => 1, cols => 3, :capture-styles);
ok $out.contains("styles={%case<flag>}"), "{%case<flag>} flag appears in legend";
}
};
subtest "snapshot-ok :capture-styles routes to caller-provided styled dir" => {
plan 4; # snapshot-ok's own pass + 3 explicit oks below
my $styled-dir = $tmp-dir ~ '-styled';
LEAVE { remove-tree($styled-dir.IO) }
my $text = Selkie::Widget::Text.new(
text => 'styled',
sizing => Sizing.fixed(1),
style => Selkie::Style.new(fg => 0xAA00AA),
);
snapshot-ok $text, 'styled-routing', :rows(1), :cols(10),
:capture-styles, dir => $styled-dir;
my $path = $styled-dir.IO.add('styled-routing.snap');
ok $path.e, 'styled snapshot file created in custom dir';
ok $path.slurp.starts-with('=== styled-snapshot v1 ==='),
'styled file starts with marker';
ok $path.slurp.contains('fg=#AA00AA'), 'styled file records the fg color';
};
# This file tests Selkie::Test::Snapshot's helper behaviour (trimming,
# file creation, match-on-second-run, styled mode) using leaf widgets
# only — keeping it strictly a unit test for the helper mechanics.
#
# Real widget-rendering regressions (Border, nested layouts, CardList,
# etc.) live under xt/snapshots/*.raku and are driven by the
# fork-per-scenario harness in xt/02-snapshots.rakutest. That harness
# isolates each render in a subprocess, which both sidesteps the MoarVM
# spesh bug on nested-widget readback and lets each scenario be
# maintained as an independent plain-text golden file. Styled scenarios
# in the same directory are auto-routed to golden-styled/ based on the
# format marker.