Selkie.git | t/ | focus-invariant.rakutest
use Test;
use lib 'lib';
use Selkie::App;
use Selkie::Widget;
use Selkie::Container;
use Selkie::Widget::Button;
use Selkie::Widget::Text;
use Selkie::Layout::VBox;
use Selkie::Sizing;
# Selkie::App's focus-invariant machinery. These tests exercise
# widget-attached() as a class method — Selkie::App.widget-attached(...)
# works against the type object so we don't need to construct an
# instance (which would require notcurses_init + a real terminal).
#
# Tiny container stub that lets us wire parent/child relationships
# without depending on a specific Layout implementation's init
# semantics.
class TestContainer does Selkie::Container {
method render() { self!render-children; self.clear-dirty }
}
plan 6;
subtest "widget-attached: unattached widget returns False" => {
plan 2;
my $w = Selkie::Widget::Button.new(label => 'x');
my $root = TestContainer.new;
nok Selkie::App.widget-attached($w, $root),
"bare widget with no parent is not attached to any root";
nok Selkie::App.widget-attached(Selkie::Widget, $root),
"type object (undefined widget) is not attached";
};
subtest "widget-attached: attached widget returns True" => {
plan 1;
my $root = TestContainer.new;
my $btn = Selkie::Widget::Button.new(label => 'x');
$root.add($btn);
ok Selkie::App.widget-attached($btn, $root),
"child widget is attached to its parent root";
};
subtest "widget-attached: deeply-nested widget returns True" => {
plan 1;
my $root = TestContainer.new;
my $mid = TestContainer.new;
my $leaf = Selkie::Widget::Button.new(label => 'x');
$root.add($mid);
$mid.add($leaf);
ok Selkie::App.widget-attached($leaf, $root),
"grandchild is attached via parent chain walk";
};
subtest "widget-attached: detached subtree returns False" => {
plan 2;
my $root-a = TestContainer.new;
my $root-b = TestContainer.new;
my $btn = Selkie::Widget::Button.new(label => 'x');
$root-a.add($btn);
ok Selkie::App.widget-attached($btn, $root-a),
"preconditions: btn is attached to root-a";
nok Selkie::App.widget-attached($btn, $root-b),
"btn is not attached to unrelated root-b";
};
subtest "widget-attached: undefined root returns False" => {
plan 1;
my $btn = Selkie::Widget::Button.new(label => 'x');
nok Selkie::App.widget-attached($btn, Selkie::Container),
"undefined root short-circuits to False";
};
# Stacked-modal topology — the key invariant App's close-modal relies
# on when popping the top modal: a focus target inside the outer
# (now-revealed) modal must validate True against that modal as root,
# but a focus target inside the popped (destroyed) inner modal must
# NOT validate against the outer modal. close-modal uses this to
# decide whether to restore the saved pre-modal-focus or fall through
# to first-focusable on the new top.
subtest "widget-attached across stacked-modal topology" => {
plan 3;
# Two independent modal trees, each with a focusable child. The
# outer tree models "the editor"; the inner tree models "the
# confirm dialog opened from inside the editor".
my $outer-modal = TestContainer.new;
my $outer-child = Selkie::Widget::Button.new(label => 'editor save');
$outer-modal.add($outer-child);
my $inner-modal = TestContainer.new;
my $inner-child = Selkie::Widget::Button.new(label => 'confirm yes');
$inner-modal.add($inner-child);
ok Selkie::App.widget-attached($outer-child, $outer-modal),
"outer-child validates against outer-modal — pre-modal-focus restoration target";
nok Selkie::App.widget-attached($inner-child, $outer-modal),
"inner-child does NOT validate against outer-modal — close-modal must fall through to first-focusable";
ok Selkie::App.widget-attached($inner-child, $inner-modal),
"inner-child validates against inner-modal — what handles inner-modal's own focus check";
};