Chandra
view release on metacpan or search on metacpan
t/96_components.t view on Meta::CPAN
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use Chandra::Component;
use Chandra::Theme;
use Chandra::Notify;
use Chandra::Tabs;
use Chandra::Nav;
use Chandra::Breadcrumb;
use Chandra::Table;
use Chandra::Form;
use Chandra::Form::Validator;
# An app that records what the components ask of it.
package MockCompApp;
our (%bound, @updates, @evals, @navigated, @css);
sub new { bless {}, shift }
sub bind { my ($self, $name, $code) = @_; $bound{$name} = $code; return $self }
sub update { my ($self, $sel, $html) = @_; push @updates, [$sel, $html]; return $self }
sub eval { my ($self, $js) = @_; push @evals, $js; return 1 }
sub navigate { my ($self, $route) = @_; push @navigated, $route; return $self }
sub css { my ($self, $css) = @_; push @css, $css; return $self }
package MockNoNav;
sub new { bless {}, shift }
sub bind { $_[0] }
sub update { $_[0] }
sub eval { 1 }
package Widget;
use Object::Proto;
BEGIN {
Object::Proto::define('Widget', extends => 'Chandra::Component', 'label:Str:default(w)');
Object::Proto::import_accessors('Widget');
}
our @events;
sub render { my ($self) = @_; return '<div class="widget"><button data-action="poke:1">' . $self->label . '</button><input data-action="typed"><select data-action="picked"></select></div>' }
sub on_mount { push @events, 'mount' }
sub on_unmount { push @events, 'unmount' }
sub on_update { push @events, 'update' }
sub on_poke { push @events, "poke:@_[1..$#_]"; return 'poked' }
package main;
my @warned;
local $SIG{__WARN__} = sub { push @warned, $_[0] };
# --- the component life cycle with a mock app ---
{
Chandra::Component->reset;
%MockCompApp::bound = ();
@MockCompApp::updates = ();
@MockCompApp::evals = ();
my $app = MockCompApp->new;
my $parent = Chandra::Component->new;
my $child = Widget->new(label => 'kid');
is($parent->child($child), $parent, 'child returns the parent');
like($parent->render, qr/<div id="_comp_\d+"><div id="_comp_\d+" class="widget">/, 'the base render wraps the children, each stamped with its id');
is($parent->update, $parent, 'update before mounting does nothing');
is($parent->update_part('x', '<b>'), $parent, 'and so does update_part');
is($parent->mount($app, '#root'), $parent, 'mount returns the component');
is(scalar @MockCompApp::updates, 2, 'the parent and the child were rendered into the app');
is($MockCompApp::updates[0][0], '#root', 'the parent into the selector');
like($MockCompApp::updates[1][0], qr/^#_comp_\d+$/, 'the child into its own id');
is_deeply(\@Widget::events, ['mount'], 'the child saw on_mount');
my ($action) = grep { /^_comp_action_/ } keys %MockCompApp::bound;
ok($action, 'an action dispatcher is bound per component');
my $child_cid = $child->_cid;
is($MockCompApp::bound{"_comp_action_$child_cid"}->('poke', 7), 'poked', 'an action reaches its handler with its arguments');
is($Widget::events[-1], 'poke:7', 'and the handler saw them');
ok(!defined $MockCompApp::bound{"_comp_action_$child_cid"}->('missing'), 'an unknown action gives undef');
like($warned[-1], qr/no handler 'on_missing' on Widget/, 'and warns');
like($MockCompApp::updates[1][1], qr/onclick="window\.chandra\.invoke\("_comp_action_$child_cid",\["poke","1"\]\)"/, 'a button action becomes an onclick');
like($MockCompApp::updates[1][1], qr/oninput="clearTimeout/, 'an input action is debounced');
like($MockCompApp::updates[1][1], qr/onchange="var _a=/, 'a select action fires on change');
is($child->update, $child, 'update on a mounted component');
is($MockCompApp::updates[-1][0], "#$child_cid", 'replaces its own element');
is($Widget::events[-1], 'update', 'and runs on_update');
$child->update_part('sub', '<b data-action="poke:2">x</b>');
like($MockCompApp::updates[-1][1], qr/onclick=/, 'update_part rewrites the actions in the html given');
is($child->_rewrite_actions('<p>plain</p>'), '<p>plain</p>', 'html without actions is left alone');
is(Chandra::Component->find_component($child_cid), $child, 'the registry finds a component by id');
is($parent->unmount, $parent, 'unmount returns the component');
is($Widget::events[-1], 'unmount', 'the child saw on_unmount');
ok(scalar(grep { /getElementById\('$child_cid'\)/ } @MockCompApp::evals), 'and its element is removed');
ok(!defined Chandra::Component->find_component($child_cid), 'and it leaves the registry');
my $orphan = Chandra::Component->new;
( run in 0.507 second using v1.01-cache-2.11-cpan-145b269f2f3 )