Testing and Validating HTML Templates

Here’s a little trick I’ve been using for a while to help ensure that a large sprawling Catalyst application always generates valid HTML. The idea is to unit test all your templates: the trick is to make it really easy with a helper class:

package My::Test::Template;

use strict;
use warnings;

use parent q(Template);

use Hash::Merge::Simple;
use Path::Class qw(dir);
use HTML::Lint::Pluggable;
use Test::More;

sub validate {
    my ( $self, $output ) = @_;

    # Routine to validate HTML5 structure

    # If this looks like an HTML fragment, wrap it in minimal tags
    if ( $output !~ m{^<html[^>]*>}ims ) {

        $output = join(
            "\n",
            '<html><head><title>Title</title></head><body>',
            $output,
            '</body></html>'
        );
    }

    my $lint = HTML::Lint::Pluggable->new();

    $lint->load_plugins('HTML5');
    $lint->only_types(HTML::Lint::Error::STRUCTURE);
    $lint->parse($output);
    $lint->eof;

    my $message = 'output is valid HTML5';
    if ( $lint->errors ) {
        for my $error ( $lint->errors ) {
            warn $error->as_string, "\n";
        }
        fail($message);
    }
    else {
        pass($message);
    }

    return $output;

} ## end sub validate

sub _init {
    my ($self, $config) = @_;

    # Modify the _init() routine from Template:
    #
    #   * add an INPUT parameter so we can specify the template file or string
    #     to test.
    #
    #   * set the default template INCLUDE_PATH and other config options to be
    #     the same as used by our Catalyst view.
    #
    $self->{INPUT} = $config->{INPUT} or die "INPUT parameter is required";

    $config = Hash::Merge::Simple::merge(
        {
            INCLUDE_PATH => [
                dir( $ENV{'PWD'}, '/root/src' )->cleanup,
                dir( $ENV{'PWD'}, '/root/lib' )->cleanup,
            ],
            TRIM        => 1,
            PRE_CHOMP   => 1,
            POST_CHOMP  => 0,
            PRE_PROCESS => 'main.tt2',
            TIMER       => 0,
        },
        $config
    );

    $self = $self->SUPER::_init($config);

    return $self;
}

sub process {
    my ( $self, $vars ) = @_;

    # Modify the process() routine from Template:
    #
    #    * make process() use the INPUT key for the template variable
    #    * die on errors rather than returning an error code
    #    * return the result of successful processing
    #    * always run the validate routine on processing a new template

    my $output = '';
    $self->SUPER::process( $self->{INPUT}, $vars, \$output )
      or die $self->error;

    return $self->validate($output);

} ## end sub process

1;

With this helper class, I can easily unit test the following template:

<div class="subnav_holder">
    <ul class="subnav">
        <li><a href="/faq">FAQ</a></li>
        [% IF has_media %]
        <li><a href="/media">In the media<a></li>
        [% END %]
        <li><a href="/about">About Us</a></li>
        <li><a href="/contact">Contact Us</a></li>
        <li><a href="/legal">Legal</a></li>
    </ul>
</div>

like so:

use strict;
use warnings;

use Test::More;

use My::Test::Template;
my $tt = My::Test::Template->new( { INPUT => 'subnav.tt2', })
    or die "$Template::ERROR\n";

unlike( $tt->process(), qr{media}ms => "Don't display media link" );

like( $tt->process( { has_media => 1 } ), qr{media}ms => 'Display media link' );

done_testing();

When I run the test I get:

ok 1 - output is valid HTML5
ok 2 - Don't display media link
 (5:45) <a> at (5:13) is never closed
 (5:45) <a> at (5:42) is never closed
not ok 3 - output is valid HTML5
#   Failed test 'output is valid HTML5'
#   at root/src/subnav.t line 37.
ok 4 - Display media link
1..4
# Looks like you failed 1 test of 4.

which highlights the HTML validation error that gets exposed if I stash “has_media”.

I really have no excuse if my app generates invalid HTML.

Happy Hacking!

Kal

Leave a comment

About Kahlil (Kal) Hodgson

user-pic Leaking memory all over the place.