=encoding UTF-8

=head1 NAME

Test2::Tools::DOM - Tools to test HTML/XML-based DOM representations

=head1 SYNOPSIS

    use Test2::V0;
    use Test2::Tools::DOM;

    my $html = <<'HTML';
    <!DOCTYPE html>
    <html lang="en-US">
        <head>
            <title>A test document</title>
            <link rel="icon" href="favicon.ico">
        </head>
        <body>
            <p class="paragraph">Some text</p>
        </body>
    </html>
    HTML

    is $html, dom {
        children bag {
            item dom { tag 'body' };
            item dom { tag 'head' };
            end;
        };

        at 'link[rel=icon]' => dom {
            attr href => 'favicon.ico'
        };

        find '.paragraph' => array {
            item dom { text 'Some text' };
            end;
        };
    };

    done_testing;

=head1 DESCRIPTION

Test2::Tools::DOM exports a set of testing functions designed to make it
easier to write declarative tests for XML-based DOM representations. This
will most commonly be HTML documents, but it can include other similar types
of documents (eg. SVG images, other XML documents, etc).

=head1 FUNCTIONS

The functions described in this section are exported by default by this
distribution.

Most of the heavy lifting behind the scenes is done by L<Mojo::DOM58>, and
most of the functions described below are thin wrappers around the methods
in that class with the same names.

Likewise, several of them support
L<CSS selectors|https://metacpan.org/pod/Mojo::DOM58#SELECTORS>
for filtering the elements they will return.

Please refer to L<that distribution's documentation|Mojo::DOM58> for
additional details.

=head2 dom

    dom { ... }

Starts a new DOM testing context. It takes a single block, inside which the
rest of the functions described in this section can be used.

It can be used as the check in any L<Test2> testing method.

The input can either be a L<Mojo::DOM58> object, or a string with the text
representation of the DOM, which will be passed to the L<Mojo::DOM58>
constructor.

For convenience, if the input is at the root node of the DOM tree, it will be
advanced to its first child element, if one exists.

=head2 all_text

    all_text CHECK

Takes a check only. Extracts the text content from all descendants of this
element (by calling
L<all_text on the Mojo::DOM58 object|Mojo::DOM58/all_text>), and this is
passed to the provided check.

    is '<p>Hello, <em>World!</em></p>', dom {
        all_text 'Hello, World!'; # OK: includes text in descendants

        text 'Hello, '; # OK: use text for the text of this element only
    };

=head2 at

    at SELECTOR, CHECK

Takes a selector and a check. The selector is used to find the first matching
descendant (by calling L<at on the Mojo::DOM58 object|Mojo::DOM58/at>), and
this is passed to the provided check.

The
L<Test2 existence checks|https://metacpan.org/pod/Test2::Tools::Compare/QUICK-CHECKS>
can be used to check whether a given selector matches or not.

    is '<div id=a><div id=b></div></div>', dom {
        attr id => 'a'; # OK, we start at #a

        at '#b' => dom {
            attr id => 'b'; # OK, we've moved to #b
        };

        at '#c' => DNE; # OK, this element does not exist
                        # A missing element matches U, F, and DNE
                        # A present element matches D, T, and E
    };

=head2 attr

    attr CHECK
    attr NAME, CHECK

Takes either a single check, or the name of an attribute and a check.

When called without a name, all attributes are fetched and passed to the
check as a hashref (by calling
L<attr on the Mojo::DOM58 object|Mojo::DOM58/attr>), and this is passed to the
provided check.

When called with a name, only the attribute with that name will be read
and passed to the check.

    is '<input type=checkbox name=answer value=42 checked>', dom {
        # Get a hashref with all attributes
        # Hashref is then checked using standard Perl logic
        attr hash {
            field type  => 'checkbox';
            field name  => 'answer';
            field value => 42;

            field checked => E; # OK: the attribute exists
            field checked => U; # OK: the attribute has no value
            field checked => F; # OK: undefined is false in Perl-land
            end;
        };
    };

When fetching a single value, the
L<Test2 boolean and existence checks|https://metacpan.org/pod/Test2::Tools::Compare/QUICK-CHECKS>
will be interpreted using XML-logic rather than Perl-logic: an attribute
without a value in the DOM will be undefined but true.

    is '<input type=checkbox name=answer value=42 checked>', dom {
        attr type    => 'checkbox';
        attr name    => 'answer';
        attr value   => 42;

        # When fetching individual attributes, checks use XML-logic
        attr checked => E; # OK: the attribute exists
        attr checked => U; # OK: the attribute has no value, so it's undefined
        attr checked => T; # OK: the attribute is present, so it's true
    };

=head2 children

    children CHECK
    children SELECTOR, CHECK

Takes either a single check, or a selector and a check.

When called without a selector, all direct children of the current element
will be passed to the check as a possibly empty arrayref (by calling
L<children on the Mojo::DOM58 object|Mojo::DOM58/children>).

When called with a selector, only children that match will be passed to the
check.

    is '<div><p>Text</p><ol><li>A</li><li>B</li></ol></div>', dom {
        children [
            # First child is <p>
            dom { tag 'p' },

            # Second child is <ol>
            dom {
                tag 'ol';

                children li => [
                    dom { text 'A' },
                    dom { text 'B' },
                ];
            },
        ];
    };

=head2 content

    content CHECK

Takes a check only. Extracts the raw content from this element and all its
descendants (by calling
L<content on the Mojo::DOM58 object|Mojo::DOM58/content>), and this is passed
to the provided check.

    is '<div>Hello, <em>World!</em></div>', dom {
        content 'Hello, <em>World!</em>';

        at em => dom { content 'World!' };
    };

=head2 find

    find SELECTOR, CHECK

Takes a selector and a check. The selector will be used to find all the
matching descendants of this elements, which will be passed to the check as a
possibly empty arrayref (by calling
L<find on the Mojo::DOM58 object|Mojo::DOM58/find>).

    is '<div><p>A</p><div><p>B</p><div><p>C</p></div></div></div>', dom {
        # Find all matching direct and indirect children
        find p => [
            dom { text 'A' },
            dom { text 'B' },
            dom { text 'C' },
        ];
    };

=head2 tag

    tag CHECK

Takes a check only. Extracts the tag of the current element (by calling
L<tag on the Mojo::DOM58 object|Mojo::DOM58/tag>), and this is passed to
the provided check.

    is '<p></p>', dom { tag 'p' };

=head2 text

    text CHECK

Takes a check only. Extracts the text content from this element only (by
calling L<text on the Mojo::DOM58 object|Mojo::DOM58/text>), and this is
passed to the provided check.

    is '<p>Hello, <em>World!</em></p>', dom {
        text 'Hello, '; # OK: 'World!' is not in this element

        all_text 'Hello, World!'; # OK: use all_text for descendants' text
    };

=head2 val

    val CHECK

I<Available from version 0.004>.

Takes a check only. Extracts the value from this element (by calling
L<'val' on the Mojo::DOM58 object|Mojo::DOM58/val>), and this is
passed to the provided check.

    is '<input type=checkbox name=answer value=42>', dom {
        val 42;

        attr value => 42; # The same, but longer
    };

=head1 SEE ALSO

=over

=item L<Test2::Tools::HTTP>

A perfect companion to this distribution: Test2::Tools::HTTP supports the
requests, Test2::Tools::DOM can be used to check the responses.

=item L<Test2::MojoX>

If you are used to using Test::Mojo and are looking for a way to use it with
the Test2 suite, then this distribution might be the right one for your needs.

=back

=head1 COPYRIGHT AND LICENSE

Copyright 2022 José Joaquín Atria

This library is free software; you can redistribute it and/or modify it under
the Artistic License 2.0.
