package Test::Mojo::Role::ElementCounter;

use Mojo::Base -base;
use Encode;
use Carp qw/croak/;
use Role::Tiny;

our $VERSION = '1.001008'; # VERSION

has _counter_selector_prefix => '';

sub dive_in {
    my ( $self, $selector ) = @_;
    $self->_counter_selector_prefix(
        $self->_counter_selector_prefix . $selector
    );
}

sub dive_out {
    my ( $self, $remove ) = @_;

    $remove = qr/\Q$remove\E$/ unless ref $remove eq 'Regexp';
    $self->_counter_selector_prefix(
        $self->_counter_selector_prefix =~ s/$remove//r,
    );
}

sub dive_up {
    shift->dive_out(qr/\S+\s*$/);
}

sub dive_reset {
    shift->_counter_selector_prefix('');
}

sub dived_text_is {
    my $self = shift;
    my @in = @_; # can't modify in-place
    $in[0] = $self->_counter_selector_prefix . $in[0];

    # Since Mojolicious 7.0 stupidly decided to stop trimming whitespace,
    # we work around it by using text_like instead with a regex that
    # does exact match, but ignores trailing/leading whitespace
    $in[1] = qr/ ^ \s* \Q$in[1]\E \s* $ /x;
    $self->text_like( @in );
}

sub element_count_is {
    my ($self, $selector, $wanted_count, $desc) = @_;

    croak 'You gave me an undefined element count that you want'
        unless defined $wanted_count;

    my $pref = $self->_counter_selector_prefix;
    $selector = join ',', map "$pref$_", split /,/,$selector;

    $desc ||= encode 'UTF-8', qq{element count for selector "$selector"};
    my $operator = $wanted_count =~ tr/<//d ? '<'
        : $wanted_count =~ tr/>//d ? '>' : '==';

    my $count = $self->tx->res->dom->find($selector)->size;
    return $self->test('cmp_ok', $count, $operator, $wanted_count, $desc);
}


q|
<Zoffix> GumbyBRAIN, Q: What did the computer do at lunchtime?
    A: Had a byte!
<GumbyBRAIN> So even that's only one byte undefined in the
    thing I ever had. Where is beer the reason siv didn't walk straight.
|;

__END__

=encoding utf8

=for stopwords Znet Zoffix app  natively

=head1 NAME

Test::Mojo::Role::ElementCounter - Test::Mojo role that provides element count tests

=head1 SYNOPSIS

Say, we need to test our app produces exactly this markup structure:

=for html  <div style="display: table; height: 91px; background: url(http://zoffix.com/CPAN/Dist-Zilla-Plugin-Pod-Spiffy/icons/section-code.png) no-repeat left; padding-left: 120px;" ><div style="display: table-cell; vertical-align: middle;">

    <ul id="products">
        <li><a href="/product/1">Product 1</a></li>
        <li>
            <a href="/products/Cat1">Cat 1</a>
            <ul>
                <li><a href="/product/2">Product 2</a></li>
                <li><a href="/product/3">Product 3</a></li>
            </ul>
        </li>
        <li><a href="/product/2">Product 2</a></li>
    </ul>

    <p>Select a product!</p>

=for html  </div></div>

The test we write:

=for html  <div style="display: table; height: 91px; background: url(http://zoffix.com/CPAN/Dist-Zilla-Plugin-Pod-Spiffy/icons/section-code.png) no-repeat left; padding-left: 120px;" ><div style="display: table-cell; vertical-align: middle;">

    use Test::More;
    use Test::Mojo::WithRoles 'ElementCounter';
    my $t = Test::Mojo::WithRoles->new('MyApp');

    $t->get_ok('/products')
    ->dive_in('#products ')
        ->element_count_is('> li', 3)
        ->dive_in('li:first-child ')
            ->element_count_is('a', 1)
            ->dived_text_is('a[href="/product/1"]' => 'Product 1')
        ->element_count_is('+ li > a', 1)
            ->dived_text_is('+ li > a[href="/products/Cat1"]' => 'Cat 1')
        ->dive_in('+ li > ul ')
            ->element_count_is('> li', 2)
            ->element_count_is('a', 2)
            ->dived_text_is('a[href="/product/2"]' => 'Product 2')
            ->dived_text_is('a[href="/product/3"]' => 'Product 3')
        ->dive_out('> ul')
        ->element_count_is('+ li a', 1);
    ->dive_reset
    ->element_count_is('#products + p', 1)
    ->text_is('#products + p' => 'Select a product!')

    done_testing;

=for html  </div></div>

=head1 SEE ALSO

Note that as of L<Mojolicious> version 6.06,
L<Test::Mojo> implements the exact match
version of C<element_count_is> natively (same method name).
This role is helpful only if you need dive methods or ranges.

=head1 DESCRIPTION

A L<Test::Mojo> role that allows you to do strict element count tests on
large structures.

=head1 METHODS

You have all the methods provided by L<Test::Mojo>, plus these:

=head2 C<element_count_is>


  $t = $t->element_count_is('.product', 6, 'we have 6 elements');
  $t = $t->element_count_is('.product', '<6', 'fewer than 6 elements');
  $t = $t->element_count_is('.product', '>6', 'more than 6 elements');

Check the count of elements specified by the selector. Second argument
is the number of elements you expect to find. The number can be
prefixed by either C<< < >> or C<< > >> to specify that you expect to
find fewer than or more than the specified number of elements.

You can shorten the selector by using C<dive_in> to store a prefix.

=head2 C<dive_in>

    $t = $t->dive_in('#products > li ');

    $t->dive_in('#products > li ')
        ->dive_in('ul > li ')
        ->element_count_is('a', 6);
        # tests: #products > li > ul > li a

To simplify selectors when testing complex structures, you can tell
the module to remember the prefix portion of the selector with
C<dive_in>. Note that multiple calls are cumulative. Use
C<dive_out>, C<dive_up>, or C<dive_reset> to go up in dive level.

B<Note:> be mindful of the last space in the selector when diving.
C<< ->dive_in('ul')->dive_in('li') >> would result in C<ulli> selector,
not C<ul li>.

B<Note:> the selector prefix only applies to C<element_count_is> and
C<dived_text_is> methods. It does not affect operation of other
methods provided by L<Test::Mojo>

=head2 C<dive_out>

    $t = $t->dive_out('li');
    $t = $t->dive_out(qr/\S+\s+(li|a)\s+$/);

    $t->dive_in('#products li ')
        ->dive_out('li'); # we're now testing: #products

Removes a portion of currently stored selector prefix (see C<dive_in>).
Takes a string or a regex as the argument that specifies
what should be removed. If a string is given, it will be taken as a literal
match to remove from I<the end> of the stored selector prefix.

=head2 C<dive_up>

    # these two are equivalent
    $t = $t->dive_up;
    $t = $t->dive_out(qr/\S+\s*$/);

Takes no arguments. A shortcut for C<< ->dive_out(qr/\S+\s*$/) >>.

=head2 C<dive_reset>

    $t = $t->dive_reset;

Resets stored selector prefix to an empty string (see C<dive_in>).

=head2 C<dived_text_is>

    $t = $t->dive('#products li:first-child ')
        ->dived_text_is('a' => 'Product 1');

Same as L<Test::Mojo>'s C<text_is> method, except the selector will
be prefixed by the stored selector prefix (see C<dive_in>).

B<NOTE:> as of version 1.001006, L<Test::Mojo>'s C<text_like> will be used
with a regex constructed to be the exact match, with any amount of whitespace
before and after the string. This is done to workaround Mojolicious Donut
breaking its whitespace handling in Mojo::DOM and by extention Test::Mojo,
and leaving useless whitespace all over the place.

=for html <div style="background: url(http://zoffix.com/CPAN/Dist-Zilla-Plugin-Pod-Spiffy/icons/hr.png);height: 18px;"></div>

=head1 REPOSITORY

=for html  <div style="display: table; height: 91px; background: url(http://zoffix.com/CPAN/Dist-Zilla-Plugin-Pod-Spiffy/icons/section-github.png) no-repeat left; padding-left: 120px;" ><div style="display: table-cell; vertical-align: middle;">

Fork this module on GitHub:
L<https://github.com/zoffixznet/Test-Mojo-Role-ElementCounter>

=for html  </div></div>

=head1 BUGS

=for html  <div style="display: table; height: 91px; background: url(http://zoffix.com/CPAN/Dist-Zilla-Plugin-Pod-Spiffy/icons/section-bugs.png) no-repeat left; padding-left: 120px;" ><div style="display: table-cell; vertical-align: middle;">

To report bugs or request features, please use
L<https://github.com/zoffixznet/Test-Mojo-Role-ElementCounter/issues>

If you can't access GitHub, you can email your request
to C<bug-test-mojo-role-elementcounter at rt.cpan.org>

=for html  </div></div>

=head1 AUTHOR

=for html  <div style="display: table; height: 91px; background: url(http://zoffix.com/CPAN/Dist-Zilla-Plugin-Pod-Spiffy/icons/section-author.png) no-repeat left; padding-left: 120px;" ><div style="display: table-cell; vertical-align: middle;">

=for html   <span style="display: inline-block; text-align: center;"> <a href="http://metacpan.org/author/ZOFFIX"> <img src="http://www.gravatar.com/avatar/328e658ab6b08dfb5c106266a4a5d065?d=http%3A%2F%2Fwww.gravatar.com%2Favatar%2F627d83ef9879f31bdabf448e666a32d5" alt="ZOFFIX" style="display: block; margin: 0 3px 5px 0!important; border: 1px solid #666; border-radius: 3px; "> <span style="color: #333; font-weight: bold;">ZOFFIX</span> </a> </span>

=for html  </div></div>

=head1 LICENSE

You can use and distribute this module under the same terms as Perl itself.
See the C<LICENSE> file included in this distribution for complete
details.

=cut