Selkie--Test--Store
NAME
Selkie::Test::Store - Conveniences for testing handlers and subscriptions
SYNOPSIS
use Test;
use Selkie::Test::Store;
my $store = mock-store(state => {
user => { name => 'Alice', roles => <admin> },
count => 0,
});
$store.register-handler('inc', -> $st, % {
(db => { count => ($st.get-in('count') // 0) + 1 },);
});
dispatch-and-tick($store, 'inc');
dispatch-and-tick($store, 'inc');
is state-at($store, 'count'), 2, 'count incremented twice';
is state-at($store, 'user', 'name'), 'Alice', 'nested state accessible';
done-testing;DESCRIPTION
Four helpers for the common test patterns against Selkie::Store:
mock-storeā build a fresh store, optionally pre-populated with nested initial statedispatch-and-tickā the dispatch + tick boilerplate in one callstate-atā splat-style read that's more readable than$store.get-in(|@path)tick-untilā tick repeatedly until a condition holds, for asynchronous completion flows
These aren't assertions themselves ā use plain is, is-deeply, etc. around the returned values. That keeps the helpers composable and lets you use whatever Test:: style fits.
EXAMPLES
Handler testing without a live App
my $store = mock-store;
my $app-handlers = My::App::StoreHandlers.new(:$db);
$app-handlers.register($store);
dispatch-and-tick($store, 'app/init');
is state-at($store, 'active-tab'), 'servers', 'init picks a default tab';
dispatch-and-tick($store, 'tab/select', name => 'logs');
is state-at($store, 'active-tab'), 'logs', 'dispatch changed tab';Waiting for an asynchronous completion
Handlers that hand work to another thread (a start block, an async effect, a database writer's .then) dispatch their completion event from that thread; it lands on a later tick. tick-until ticks and polls until the observable state change arrives:
dispatch-and-tick($store, 'thing/save-requested', thing => $thing);
ok tick-until($store, {
!(state-at($store, 'ui', 'thing', 'saving') // False)
}), 'save completed asynchronously';Subscribing a widget in a test
my $text = Selkie::Widget::Text.new(text => '-', sizing => Sizing.fixed(1));
my $store = mock-store;
$store.subscribe-with-callback(
'mirror',
-> $s { $s.get-in('count') // 0 },
-> $n { $text.set-text("count: $n") },
$text,
);
dispatch-and-tick($store, 'inc'); # (assuming handler registered elsewhere)
is $text.text, 'count: 1', 'subscription callback fired';SEE ALSO
Selkie::Store ā the reactive store being tested
Selkie::Test::Keys, Selkie::Test::Supply ā for UI-level testing
sub mock-store
sub mock-store(
:%state
) returns Selkie::StoreCreate a fresh store, optionally pre-populated with nested state. The :state hash is deep-merged into the store's db in one shot ā nested hashes become nested paths. my $store = mock-store(state => { app => { count => 0, user => { name => 'Alice' } }, flag => True, });
sub dispatch-and-tick
sub dispatch-and-tick(
Selkie::Store $store,
Str:D $event,
*%payload
) returns MuDispatch an event and immediately tick the store, replacing the common two-liner in tests. dispatch-and-tick(store, 'user/set', name => 'Bob'); Equivalent to: event, |%payload); $store.tick;
sub state-at
sub state-at(
Selkie::Store $store,
*@path
) returns MuSplat-style deep read. Same semantics as $store.get-in(|@path) but easier on the eyes in tests: is state-at(store, 'counter'), 0, 'reset';
sub tick-until
sub tick-until(
Selkie::Store $store,
&done,
Int :$tries = 80,
Real :$delay = 0.025
) returns BoolTick the store until &done returns true or :tries runs out. For flows whose completion event arrives from another thread (a start block, an async effect, a DB writer's .then): each round ticks the store ā picking up any cross-thread dispatches ā checks the condition, then sleeps :delay seconds. Returns the final truth of &done, so a timeout reports as a false assertion at the call site rather than a throw: dispatch-and-tick(store, { !(state-at($store, 'ui', 'thing', 'saving') // False) }), 'save completed';