Selkie.git | t/ | 58-image-blit-cache.rakutest
use Test;
use lib 'lib';
use Selkie::Widget::Image;
use Selkie::Sizing;
# Image's blit-plane lifecycle is now self-contained inside its render
# method. This file exercises the contract Image exposes to apps —
# set-file / clear-image / park / has-blit-plane / destroy — without
# touching a live notcurses plane (the full blit path needs a sprixel-
# capable terminal, which doesn't fit a headless test).
plan 7;
subtest 'fresh Image has no live blit-plane' => {
plan 1;
my $img = Selkie::Widget::Image.new(sizing => Sizing.fixed(5));
nok $img.has-blit-plane,
'no blit-plane on a freshly-constructed Image';
};
subtest 'set-file updates .file but does not eagerly create a blit' => {
plan 2;
my $img = Selkie::Widget::Image.new(sizing => Sizing.fixed(5));
$img.set-file('/foo.png');
is $img.file, '/foo.png', 'file accessor reflects the new path';
nok $img.has-blit-plane,
'set-file does not eagerly create or destroy a blit-plane';
};
subtest 'clear-image clears .file but leaves blit-plane state alone' => {
plan 2;
my $img = Selkie::Widget::Image.new(sizing => Sizing.fixed(5));
$img.set-file('/foo.png');
$img.clear-image;
is $img.file, Str, 'file is cleared';
nok $img.has-blit-plane,
'clear-image does not flip the blit-plane state on its own';
};
subtest 'destroy-blit-plane is idempotent' => {
plan 1;
my $img = Selkie::Widget::Image.new(sizing => Sizing.fixed(5));
lives-ok { $img.destroy-blit-plane; $img.destroy-blit-plane },
'destroy-blit-plane on an already-empty Image is safe';
};
subtest 'park is idempotent on an unattached widget' => {
plan 2;
# park is called by container widgets (CardList off-screen items)
# to push an Image past the terminal viewport. On an unattached
# widget (no parent, no plane) it must early-return cleanly.
my $img = Selkie::Widget::Image.new(sizing => Sizing.fixed(5));
lives-ok { $img.park; $img.park; $img.park },
'park on an unattached widget is safe to call repeatedly';
nok $img.has-blit-plane,
'no blit-plane after park';
};
subtest 'no-op park emits no trace span' => {
plan 2;
# Parked image cards re-enter destroy-blit-plane on every layout
# pass (VBox/HBox park zero-sized children each render). The no-op
# guard must run BEFORE span creation, or a session drowns in
# events recording nothing — the 2026-07 capture had 138k of them
# at 98.4% no-op. Contract: a park with no live blit leaves ZERO
# image.destroy-blit-plane events in the trace.
use Selkie::Trace;
my $path = $*TMPDIR.add("selkie-park-span-{$*PID}-{now.Int}.json").Str;
Selkie::Trace.init(mode => 'trace', trace-path => $path);
my $img = Selkie::Widget::Image.new(sizing => Sizing.fixed(5));
$img.park for ^50;
$img.destroy-blit-plane for ^50;
Selkie::Trace.shutdown;
my $json = $path.IO.slurp;
unlike $json, /'image.destroy-blit-plane'/,
'no destroy-blit-plane spans for 100 no-op calls';
like $json, /'trace/shutdown'/, 'trace file is otherwise intact';
$path.IO.unlink if $path.IO.e;
};
subtest 'set-file twice with the same path is a no-op' => {
plan 2;
my $img = Selkie::Widget::Image.new(sizing => Sizing.fixed(5));
$img.set-file('/foo.png');
$img.set-file('/foo.png');
is $img.file, '/foo.png', 'file unchanged';
nok $img.has-blit-plane, 'still no blit (no plane attached)';
};
done-testing;