Selkie.git | t/ | 83-border-style.rakutest


use Test;
use lib 'lib';

use Selkie::BorderStyle;
use Selkie::Widget::Border;

# Glyph-set plumbing: the tables themselves, the .for() cache, and the
# override precedence Border applies between an explicit glyph table
# and a BorderKind. Nothing here needs a plane — BorderGlyphs is a
# plain value class and effective-glyphs is pure.

plan 10;

# Kind => (top-left, top-right, bottom-left, bottom-right, horizontal,
# vertical). Pinning the literal glyphs is the point of the test: these
# are load-bearing for every Border snapshot in the distro and for
# downstream apps' goldens.
my @tables =
    BorderSingle,  <┌ ┐ └ ┘ ─ │>,
    BorderRounded, <╭ ╮ ╰ ╯ ─ │>,
    BorderDouble,  <╔ ╗ ╚ ╝ ═ ║>,
    BorderHeavy,   <┏ ┓ ┗ ┛ ━ ┃>,
    BorderAscii,   ('+', '+', '+', '+', '-', '|'),
;

subtest "stock glyph tables" => {
    plan @tables.elems div 2;
    for @tables -> $kind, @g {
        subtest "$kind" => {
            plan 6;
            my $t = BorderGlyphs.for($kind);
            is $t.top-left,     @g[0], "top-left";
            is $t.top-right,    @g[1], "top-right";
            is $t.bottom-left,  @g[2], "bottom-left";
            is $t.bottom-right, @g[3], "bottom-right";
            is $t.horizontal,   @g[4], "horizontal";
            is $t.vertical,     @g[5], "vertical";
        };
    }
};

subtest "every glyph is exactly one character" => {
    plan @tables.elems div 2;
    # A multi-column glyph would shear the frame: the horizontal run is
    # emitted as one putstr of cols-2 copies.
    for @tables -> $kind, @ {
        my $t = BorderGlyphs.for($kind);
        my @lens = ($t.top-left, $t.top-right, $t.bottom-left,
                    $t.bottom-right, $t.horizontal, $t.vertical).map(*.chars);
        is-deeply @lens, [1, 1, 1, 1, 1, 1], "$kind glyphs are single characters";
    }
};

subtest "BorderAscii is 7-bit clean" => {
    plan 1;
    my $t = BorderGlyphs.for(BorderAscii);
    my $all = $t.top-left ~ $t.top-right ~ $t.bottom-left
            ~ $t.bottom-right ~ $t.horizontal ~ $t.vertical;
    ok $all.ords.all < 128, "no codepoint above 127 — the escape hatch works";
};

subtest ".for is cached" => {
    plan 2;
    ok BorderGlyphs.for(BorderRounded) === BorderGlyphs.for(BorderRounded),
        "same kind returns the identical object";
    nok BorderGlyphs.for(BorderRounded) === BorderGlyphs.for(BorderDouble),
        "different kinds are different objects";
};

subtest "factory methods match .for" => {
    plan 5;
    ok BorderGlyphs.single.horizontal  eq BorderGlyphs.for(BorderSingle).horizontal,  "single";
    ok BorderGlyphs.rounded.top-left   eq BorderGlyphs.for(BorderRounded).top-left,   "rounded";
    ok BorderGlyphs.double.vertical    eq BorderGlyphs.for(BorderDouble).vertical,    "double";
    ok BorderGlyphs.heavy.bottom-right eq BorderGlyphs.for(BorderHeavy).bottom-right, "heavy";
    ok BorderGlyphs.ascii.top-right    eq BorderGlyphs.for(BorderAscii).top-right,    "ascii";
};

subtest "custom glyph tables" => {
    plan 3;
    my $g = BorderGlyphs.new(
        top-left => '.', top-right    => '.',
        bottom-left => "'", bottom-right => "'",
        horizontal  => '.', vertical     => ':',
    );
    is $g.top-left, '.', "custom glyph stored";
    is $g.vertical, ':', "custom vertical stored";

    my $derived = BorderGlyphs.for(BorderSingle).clone(top-left => '◤');
    is-deeply
        ($derived.top-left, $derived.top-right, $derived.horizontal),
        ('◤', '┐', '─'),
        "clone varies one glyph and keeps the rest";
};

subtest "all six glyphs are required" => {
    plan 1;
    dies-ok { BorderGlyphs.new(top-left => '+', top-right => '+') },
        "a partial table is a construction error, not a half-drawn frame";
};

subtest "Border defaults to BorderSingle" => {
    plan 3;
    my $b = Selkie::Widget::Border.new;
    is $b.border-style, BorderSingle, "default kind";
    nok $b.border-glyphs.defined, "no explicit glyph table by default";
    ok $b.effective-glyphs === BorderGlyphs.for(BorderSingle),
        "effective-glyphs resolves to the single-line table";
};

subtest "effective-glyphs precedence" => {
    plan 4;
    my $custom = BorderGlyphs.new(
        top-left => 'a', top-right    => 'b',
        bottom-left => 'c', bottom-right => 'd',
        horizontal  => 'e', vertical     => 'f',
    );
    my $b = Selkie::Widget::Border.new(border-style => BorderDouble);
    ok $b.effective-glyphs === BorderGlyphs.for(BorderDouble),
        "kind wins when no explicit table is set";

    $b.set-border-glyphs($custom);
    ok $b.effective-glyphs === $custom,
        "explicit table beats the kind";

    $b.set-border-style(BorderHeavy);
    ok $b.effective-glyphs === $custom,
        "changing the kind under an explicit table changes nothing visible";

    $b.clear-border-glyphs;
    ok $b.effective-glyphs === BorderGlyphs.for(BorderHeavy),
        "clearing the table falls back to the current kind";
};

subtest "glyph setters mark dirty" => {
    plan 4;
    my $b = Selkie::Widget::Border.new;
    my $custom = BorderGlyphs.for(BorderAscii);

    $b.clear-dirty;
    $b.set-border-style(BorderRounded);
    ok $b.is-dirty, "set-border-style marks dirty";

    $b.clear-dirty;
    $b.set-border-glyphs($custom);
    ok $b.is-dirty, "set-border-glyphs marks dirty";

    $b.clear-dirty;
    $b.clear-border-glyphs;
    ok $b.is-dirty, "clear-border-glyphs marks dirty";

    # Constructor-set kind must be honoured too — Mindmoor/Cantina set
    # it at build time and never call the setter.
    my $c = Selkie::Widget::Border.new(border-style => BorderAscii);
    ok $c.effective-glyphs === BorderGlyphs.for(BorderAscii),
        "constructor-set kind resolves";
};