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;