Selkie.git | t/ | 22-viewport.rakutest
use Test;
use lib 'lib';
use Selkie::Widget;
use Selkie::Container;
use Selkie::Sizing;
class TestWidget does Selkie::Widget {
method render() { self.clear-dirty }
}
class TestContainer does Selkie::Container {
method render() { self!render-children; self.clear-dirty }
}
plan 7;
subtest "widget viewport defaults to zero" => {
plan 4;
my $w = TestWidget.new;
is $w.abs-y, 0, "abs-y default";
is $w.abs-x, 0, "abs-x default";
is $w.viewport-rows, 0, "viewport-rows default";
is $w.viewport-cols, 0, "viewport-cols default";
};
subtest "set-viewport sets values" => {
plan 4;
my $w = TestWidget.new;
$w.set-viewport(abs-y => 10, abs-x => 20, rows => 30, cols => 40);
is $w.abs-y, 10, "abs-y set";
is $w.abs-x, 20, "abs-x set";
is $w.viewport-rows, 30, "viewport-rows set";
is $w.viewport-cols, 40, "viewport-cols set";
};
subtest "VBox propagates viewport to children" => {
plan 4;
use Selkie::Layout::VBox;
my $vbox = Selkie::Layout::VBox.new(sizing => Sizing.flex);
$vbox.set-viewport(abs-y => 5, abs-x => 10, rows => 20, cols => 40);
my $child1 = TestWidget.new(sizing => Sizing.fixed(8));
my $child2 = TestWidget.new(sizing => Sizing.fixed(12));
$vbox.add($child1);
$vbox.add($child2);
# Simulate layout by giving vbox a plane-like size
$vbox.resize(20, 40);
# VBox layout runs during render — we need to trigger it
# Since we can't render without a plane, check that set-viewport
# was called on children after a manual layout call would run
# For now just verify viewport is accessible
ok $child1.can('abs-y'), "child has viewport accessor";
ok $child1.can('viewport-rows'), "child has viewport-rows accessor";
ok $child2.can('abs-y'), "child2 has viewport accessor";
ok $child2.can('viewport-rows'), "child2 has viewport-rows accessor";
};
subtest "widget-id is unique" => {
plan 2;
my $w1 = TestWidget.new;
my $w2 = TestWidget.new;
ok $w1.widget-id != $w2.widget-id, "different IDs";
ok $w1.widget-id > 0, "positive ID";
};
subtest "widget-id on containers" => {
plan 1;
my $c = TestContainer.new;
ok $c.widget-id > 0, "container has ID";
};
subtest "set-viewport marks dirty on absolute-position change" => {
plan 5;
# Image sprixels don't follow notcurses plane moves — when a
# parent layout shifts a card, the host Image's render has to
# fire so its blit-plane teardown / re-blit logic can run.
# Marking dirty on a viewport reposition guarantees that.
my $w = TestWidget.new;
$w.set-viewport(abs-y => 5, abs-x => 5, rows => 10, cols => 10);
ok $w.is-dirty, "abs change from default (0,0) to (5,5) marks dirty";
$w.clear-dirty;
$w.set-viewport(abs-y => 5, abs-x => 5, rows => 10, cols => 10);
nok $w.is-dirty, "no-op set-viewport leaves the widget clean";
$w.clear-dirty;
$w.set-viewport(abs-y => 7, abs-x => 5, rows => 10, cols => 10);
ok $w.is-dirty, "abs-y change marks dirty";
$w.clear-dirty;
$w.set-viewport(abs-y => 7, abs-x => 9, rows => 10, cols => 10);
ok $w.is-dirty, "abs-x change marks dirty";
$w.clear-dirty;
$w.set-viewport(abs-y => 7, abs-x => 9, rows => 20, cols => 30);
nok $w.is-dirty,
"rows/cols change without abs change does not dirty (handle-resize covers that path)";
};
subtest "set-viewport rejects pathological Int values via safe-coord gate" => {
plan 8;
# Regression: a MoarVM spesh mis-specialisation upstream can leave
# the Int slot holding a multi-thousand-bit bigint (see the
# memoization-revert note in App::Mindmoor::View::TaskRow). The
# renderer must not crash. Earlier behavior was "store the big
# value and rely on boxed-Int dispatch to keep the comparison
# working" — but that left the assignment site (line 427's
# `$!abs-y = $abs-y`) crashing on Scalar.STORE's inlined unbox.
#
# Current behavior: safe-coord uses nqp::isbig_I to detect any value
# that doesn't fit in int64 and signals corruption via Nil.
# set-viewport then emits an opt-in diagnostic and skips the
# update. The widget keeps its previous clean coords, the parent
# layout re-passes coords on the next render, and recovery is
# automatic if the upstream spesh slot has cleared.
my $w = TestWidget.new;
# Seed a known-good baseline so we can verify the rejected update
# leaves the slot untouched.
$w.set-viewport(:abs-y(7), :abs-x(11), :rows(10), :cols(10));
is $w.abs-y, 7, "baseline abs-y stored cleanly";
is $w.abs-x, 11, "baseline abs-x stored cleanly";
my Int $huge = 2 ** 3273;
my $diag = $*TMPDIR.add("selkie-viewport-diag-{$*PID}.log");
$diag.unlink if $diag.e;
{
temp %*ENV<SELKIE_DIAGNOSTIC_LOG> = $diag.Str;
lives-ok {
$w.set-viewport(:abs-y($huge), :abs-x(0), :rows(10), :cols(10));
}, "huge abs-y is rejected without crashing";
}
ok $diag.slurp.contains("skipping corrupt coord"), "rejection diagnostic written when explicitly requested";
$diag.unlink if $diag.e;
is $w.abs-y, 7, "rejected coord doesn't overwrite the prior clean abs-y";
is $w.abs-x, 11, "rejected coord doesn't overwrite the prior clean abs-x";
# Recovery: the next set-viewport call with sane values must work
# exactly as if the corrupt call never happened.
lives-ok {
$w.set-viewport(:abs-y(42), :abs-x(99), :rows(10), :cols(10));
}, "follow-up call with sane coords succeeds after a rejected one";
is-deeply (:y($w.abs-y), :x($w.abs-x)).hash, (:y(42), :x(99)).hash,
"post-recovery slot holds the sane coords";
};