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\(&quot;_comp_action_$child_cid&quot;,\[&quot;poke&quot;,&quot;1&quot;\]\)"/, '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 )