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
Hi Mal,
Would it not be easier via Mech and testing - >content? What's the benefit of your way?
Thanks.
Kal, rather. Sorry.