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";
};