Selkie.git | t/ | 73-widget-destroy-unsubscribes.rakutest


use Test;
use lib 'lib';

use Selkie::Store;
use Selkie::Widget;
use Selkie::Container;

# Widget.destroy must unsubscribe the widget from the store. Without
# this, the store's subscription Hash pins the widget alive (the sub
# entry holds widget => $widget) and GC never collects it — so
# DESTROY/destroy doesn't fire, the plane handle leaks, and on every
# modal cycle / screen swap / dynamic rebuild we accumulate dead
# notcurses planes.

class W does Selkie::Widget {
    method render() { self.clear-dirty }
}

class C does Selkie::Container {
    method render() { self.clear-dirty }
}

plan 4;

subtest "destroy unsubscribes a path subscription" => {
    plan 2;
    my $s = Selkie::Store.new;
    my $w = W.new;
    $w.set-store($s);

    $s.subscribe('watch', ['path'], $w);
    is $s.subscription-count, 1, "subscription registered";

    $w.destroy;
    is $s.subscription-count, 0,
        "destroy removed the subscription via the !destroy-plane chokepoint";
};

subtest "destroy unsubscribes a computed subscription" => {
    plan 2;
    my $s = Selkie::Store.new;
    my $w = W.new;
    $w.set-store($s);

    $s.subscribe-computed('computed', -> $st { $st.get-in('a') }, $w);
    is $s.subscription-count, 1, "computed sub registered";

    $w.destroy;
    is $s.subscription-count, 0, "destroy removed the computed sub";
};

subtest "destroying a container cascades unsubscribe to children" => {
    plan 3;
    my $s = Selkie::Store.new;
    my $c = C.new;
    my $w1 = W.new;
    my $w2 = W.new;
    $c.set-store($s);
    $c.add($w1);
    $c.add($w2);

    $s.subscribe('w1', ['a'], $w1);
    $s.subscribe('w2', ['b'], $w2);
    $s.subscribe('c',  ['root'], $c);
    is $s.subscription-count, 3, "3 subscriptions registered";

    $c.destroy;
    is $s.subscription-count, 0,
        "container destroy unsubscribes itself AND every child";

    # The destroy cascade is also safe to repeat (idempotent) — used
    # by re-entrant shutdown paths.
    lives-ok { $c.destroy }, "second destroy is a no-op";
};

subtest "destroy is safe when the widget has no store" => {
    plan 1;
    my $w = W.new;   # no set-store call
    lives-ok { $w.destroy },
        "destroy on a store-less widget is a no-op";
};