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