Selkie.git | t/ | 27-checkbox.rakutest


use Test;
use lib 'lib';

use Selkie::Widget::Checkbox;
use Selkie::Event;
use Selkie::Layout::VBox;
use Selkie::Sizing;
use Selkie::Test::Keys;
use Selkie::Test::Snapshot;
use Notcurses::Native::Types;

plan 20;

subtest "construction with defaults" => {
    plan 4;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Enable');
    is $cb.label, 'Enable', "label set";
    ok $cb.focusable, "focusable by default";
    nok $cb.checked, "unchecked by default";
    nok $cb.is-focused, "not focused initially";
};

subtest "on-change returns Supply" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    isa-ok $cb.on-change, Supply, "on-change is Supply";
};

subtest "toggle flips checked state" => {
    plan 2;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.toggle;
    ok $cb.checked, "checked after first toggle";
    $cb.toggle;
    nok $cb.checked, "unchecked after second toggle";
};

subtest "toggle emits on-change" => {
    plan 2;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    my @values;
    $cb.on-change.tap: -> $v { @values.push($v) };
    $cb.toggle;
    $cb.toggle;
    is @values[0], True, "first emit is True";
    is @values[1], False, "second emit is False";
};

subtest "set-checked updates state" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.set-checked(True);
    ok $cb.checked, "checked after set-checked(True)";
};

subtest "set-checked emits on-change" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    my $emitted = False;
    $cb.on-change.tap: -> $ { $emitted = True };
    $cb.set-checked(True);
    ok $emitted, "on-change emitted";
};

subtest "set-checked with same value does not emit" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    my $count = 0;
    $cb.on-change.tap: -> $ { $count++ };
    $cb.set-checked(False);
    is $count, 0, "no emit when setting same value";
};

subtest "space toggles when focused" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.set-focused(True);
    $cb.handle-event(key-event(id => NCKEY_SPACE));
    ok $cb.checked, "space toggles checkbox";
};

subtest "enter toggles when focused" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.set-focused(True);
    $cb.handle-event(key-event(id => NCKEY_ENTER));
    ok $cb.checked, "enter toggles checkbox";
};

subtest "no toggle when not focused" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.handle-event(key-event(id => NCKEY_SPACE));
    nok $cb.checked, "no toggle when unfocused";
};

subtest "set-focused marks dirty" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.clear-dirty;
    $cb.set-focused(True);
    ok $cb.is-dirty, "dirty after focus change";
};

subtest "toggle marks dirty" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.clear-dirty;
    $cb.toggle;
    ok $cb.is-dirty, "dirty after toggle";
};

subtest "unrelated key not consumed" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.set-focused(True);
    nok $cb.handle-event(key-event(id => 'x'.ord)), "x not consumed";
};

subtest "keybind on checkbox works" => {
    plan 1;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.set-focused(True);
    my $called = False;
    $cb.on-key: 'ctrl+q', -> $ { $called = True };
    $cb.handle-event(key-event(id => 'q'.ord, modifiers => Set(Mod-Ctrl)));
    ok $called, "keybind fires";
};

subtest "default debounce collapses rapid user toggles" => {
    plan 4;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    my @values;
    $cb.on-change.tap: -> $v { @values.push($v) };

    $cb.handle-event(mouse-event(id => NCKEY_BUTTON1, input-type => NCTYPE_PRESS));
    $cb.handle-event(mouse-event(id => NCKEY_BUTTON1, input-type => NCTYPE_PRESS));
    ok $cb.checked, "rapid duplicate click leaves checkbox checked";
    is @values.elems, 1, "duplicate click emits once";

    sleep 0.15;
    $cb.handle-event(mouse-event(id => NCKEY_BUTTON1, input-type => NCTYPE_PRESS));
    nok $cb.checked, "click after debounce window toggles";

    sleep 0.15;
    $cb.set-focused(True);
    $cb.handle-event(key-event(id => NCKEY_SPACE));
    $cb.handle-event(key-event(id => NCKEY_SPACE));
    is @values.elems, 3, "rapid duplicate key toggle emits once";
};

subtest "debounce-ms 0 allows every user toggle" => {
    plan 2;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test', debounce-ms => 0);
    my $count = 0;
    $cb.on-change.tap: -> $ { $count++ };

    $cb.handle-event(mouse-event(id => NCKEY_BUTTON1, input-type => NCTYPE_PRESS));
    $cb.handle-event(mouse-event(id => NCKEY_BUTTON1, input-type => NCTYPE_PRESS));
    is $count, 2, "rapid clicks both emit when debounce disabled";
    nok $cb.checked, "two rapid clicks toggle twice";
};

subtest "default glyphs are byte-identical to the historical hardcoded pair" => {
    plan 2;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    is $cb.checked-glyph, '[x]', "default checked-glyph is [x]";
    is $cb.unchecked-glyph, '[ ]', "default unchecked-glyph is [ ]";
};

subtest "set-glyphs updates both glyphs and marks dirty" => {
    plan 3;
    my $cb = Selkie::Widget::Checkbox.new(label => 'Test');
    $cb.clear-dirty;
    $cb.set-glyphs(checked => '[✓]', unchecked => '[ ]');
    is $cb.checked-glyph, '[✓]', "checked-glyph updated";
    is $cb.unchecked-glyph, '[ ]', "unchecked-glyph updated";
    ok $cb.is-dirty, "dirty after set-glyphs";
};

subtest "custom glyphs render in place of the defaults" => {
    plan 2;
    my $checked-cb = Selkie::Widget::Checkbox.new(label => 'On');
    $checked-cb.set-glyphs(checked => '(•)', unchecked => '( )');
    $checked-cb.set-checked(True);
    my $checked-out = render-to-string($checked-cb, rows => 1, cols => 20);
    is $checked-out, '(•) On', "checked custom glyph renders";

    my $unchecked-cb = Selkie::Widget::Checkbox.new(label => 'Off');
    $unchecked-cb.set-glyphs(checked => '(•)', unchecked => '( )');
    my $unchecked-out = render-to-string($unchecked-cb, rows => 1, cols => 20);
    is $unchecked-out, '( ) Off', "unchecked custom glyph renders";
};

subtest "a disabled checkbox leaves the focus cycle but stays focusable" => {
    plan 5;
    my $vbox = Selkie::Layout::VBox.new(sizing => Sizing.flex);
    my $cb = Selkie::Widget::Checkbox.new(label => 'Enable', sizing => Sizing.fixed(1));
    $vbox.add($cb);

    nok $cb.disabled, 'not disabled by default';
    is $vbox.focusable-descendants.List.elems, 1, 'reachable by Tab';

    $cb.set-disabled(True);
    is $vbox.focusable-descendants.List.elems, 0, 'disabled drops out of the cycle';
    ok $cb.focusable, 'but focusable is untouched';

    $cb.set-disabled(False);
    is $vbox.focusable-descendants.List.elems, 1, 're-enabling puts it back';
};