Selkie.git | t/ | 20-store-integration.rakutest


use Test;
use lib 'lib';

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

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

class AutoSubWidget does Selkie::Widget {
    method on-store-attached($store) {
        self.once-subscribe('auto-sub', 'data');
    }

    method render() { self.clear-dirty }
}

class TestContainer does Selkie::Container {
    method render() { self!render-children; self.clear-dirty }
}

plan 12;

subtest "widget has no store by default" => {
    plan 1;
    my $w = TestWidget.new;
    nok $w.store.defined, "no store on standalone widget";
};

subtest "store propagates on add" => {
    plan 2;
    my $store = Selkie::Store.new;
    my $c = TestContainer.new;
    $c.set-store($store);
    my $w = TestWidget.new;
    $c.add($w);
    ok $w.store === $store, "child got store from parent";
    ok $w.store.defined, "store is defined";
};

subtest "store propagates to nested children" => {
    plan 1;
    my $store = Selkie::Store.new;
    my $outer = TestContainer.new;
    $outer.set-store($store);
    my $inner = TestContainer.new;
    my $leaf = TestWidget.new;
    $inner.add($leaf);
    $outer.add($inner);
    ok $leaf.store === $store, "deeply nested child got store";
};

subtest "widget.dispatch routes to store" => {
    plan 1;
    my $store = Selkie::Store.new;
    $store.register-handler('test', -> $store, %ev {
        (db => { got-it => True },);
    });
    my $c = TestContainer.new;
    $c.set-store($store);
    my $w = TestWidget.new;
    $c.add($w);
    $w.dispatch('test');
    $store.tick;
    ok $store.get-in('got-it'), "dispatch from widget reached store";
};

subtest "widget.subscribe creates subscription" => {
    plan 2;
    my $store = Selkie::Store.new;
    my $c = TestContainer.new;
    $c.set-store($store);
    my $w = TestWidget.new;
    $c.add($w);
    $w.subscribe('my-sub', 'data', 'name');
    $w.clear-dirty;

    $store.register-handler('set', -> $store, %ev {
        (db => { data => { name => 'hello' } },);
    });
    $store.dispatch('set');
    $store.tick;
    ok $w.is-dirty, "widget dirtied by subscription";

    $w.clear-dirty;
    $store.tick;
    nok $w.is-dirty, "not dirty when unchanged";
};

subtest "widget.subscribe-computed works" => {
    plan 1;
    my $store = Selkie::Store.new;
    my $c = TestContainer.new;
    $c.set-store($store);
    my $w = TestWidget.new;
    $c.add($w);
    $w.subscribe-computed('derived', -> $store {
        ($store.get-in('x') // 0) + ($store.get-in('y') // 0);
    });
    $w.clear-dirty;

    $store.register-handler('set-xy', -> $store, %ev {
        (db => { x => 10, y => 20 },);
    });
    $store.dispatch('set-xy');
    $store.tick;
    ok $w.is-dirty, "computed subscription fired";
};

subtest "dispatch without store is safe" => {
    plan 1;
    my $w = TestWidget.new;
    lives-ok { $w.dispatch('anything') }, "dispatch on storeless widget doesn't die";
};

subtest "subscribe without store is safe" => {
    plan 1;
    my $w = TestWidget.new;
    lives-ok { $w.subscribe('x', 'path') }, "subscribe on storeless widget doesn't die";
};

subtest "once-subscribe without store does not reserve the id" => {
    plan 2;
    my $store = Selkie::Store.new;
    my $w = TestWidget.new;

    lives-ok { $w.once-subscribe('late-sub', 'data') },
        "once-subscribe on storeless widget doesn't die";

    $w.set-store($store);
    $w.once-subscribe('late-sub', 'data');
    is $store.subscription-count, 1, "same id can subscribe after store attach";
};

subtest "once subscriptions re-register when the attached store changes" => {
    plan 6;
    my $store-a = Selkie::Store.new;
    my $store-b = Selkie::Store.new;
    my $w = AutoSubWidget.new;

    for $store-a, $store-b -> $store {
        $store.register-handler('set-data', -> $s, %ev {
            (db => { data => %ev<value> },);
        });
    }

    $w.set-store($store-a);
    is $store-a.subscription-count, 1, "initial store registered one subscription";

    $w.set-store($store-a);
    is $store-a.subscription-count, 1, "same-store reattach stays idempotent";

    $w.set-store($store-b);
    is $store-a.subscription-count, 0, "old store unsubscribed on store change";
    is $store-b.subscription-count, 1, "new store registered the once subscription";

    $w.clear-dirty;
    $store-a.dispatch('set-data', value => 'old');
    $store-a.tick;
    nok $w.is-dirty, "old store no longer notifies the widget";

    $store-b.dispatch('set-data', value => 'new');
    $store-b.tick;
    ok $w.is-dirty, "new store notifies the widget";
};

subtest "remove unsubscribes widget" => {
    plan 1;
    my $store = Selkie::Store.new;
    my $c = TestContainer.new;
    $c.set-store($store);
    my $w = TestWidget.new;
    $c.add($w);
    $w.subscribe('temp-sub', 'data');
    $c.remove($w);

    $w.clear-dirty;
    $store.register-handler('set', -> $store, %ev {
        (db => { data => 'changed' },);
    });
    $store.dispatch('set');
    $store.tick;
    nok $w.is-dirty, "removed widget not notified";
};

subtest "clear unsubscribes all children" => {
    plan 1;
    my $store = Selkie::Store.new;
    my $c = TestContainer.new;
    $c.set-store($store);
    my $w1 = TestWidget.new;
    my $w2 = TestWidget.new;
    $c.add($w1);
    $c.add($w2);
    $w1.subscribe('sub1', 'data');
    $w2.subscribe('sub2', 'data');
    $c.clear;

    $w1.clear-dirty;
    $w2.clear-dirty;
    $store.register-handler('set', -> $store, %ev {
        (db => { data => 'changed' },);
    });
    $store.dispatch('set');
    $store.tick;
    nok $w1.is-dirty || $w2.is-dirty, "cleared children not notified";
};