package Test::XHTML;
use strict;
use warnings;
use vars qw($VERSION);
$VERSION = '0.12';
#----------------------------------------------------------------------------
=head1 NAME
Test::XHTML - Test web page code validation.
=head1 SYNOPSIS
use Test::XHTML;
my $tests = "t/102-internal-level7.csv";
Test::XHTML::runtests($tests);
=head1 DESCRIPTION
Test the validation of a list of URLs. This includes DTD Validation, WAI WCAG
v2.0 compliance and basic Best Practices.
=cut
# -------------------------------------
# Library Modules
use IO::File;
use Data::Dumper;
use Test::Builder;
use Test::XHTML::Valid;
use Test::XHTML::WAI;
use Test::XHTML::Critic;
use WWW::Mechanize;
# -------------------------------------
# Singletons
my $mech = WWW::Mechanize->new();
my $txv = Test::XHTML::Valid->new(mech => $mech);
my $txw = Test::XHTML::WAI->new();
my $txc = Test::XHTML::Critic->new();
my $Test = Test::Builder->new();
sub import {
my $self = shift;
my $caller = caller;
no strict 'refs';
*{$caller.'::runtests'} = \&runtests;
*{$caller.'::setlog'} = \&setlog;
my @args = @_;
$Test->exported_to($caller);
$Test->plan(@args) if(@args);
}
# -------------------------------------
# Public Methods
sub runtests {
my $tests = shift;
my ($link,$type,$content,%config,@all);
my $fh = IO::File->new($tests,'r') or die "Cannot open file [$tests]: $!\n";
while(<$fh>) {
s/\s*$//;
s/^[#,].*$//;
next if(/^\s*$/);
my ($cmd,$text,$label) = split(',',$_,3);
#$cmd =~ s/\s*$//;
#$Test->diag("cmd=[$cmd], text=[$text], label=[$label]");
if($cmd eq 'config') {
my ($key,$value) = split('=',$text,2);
$config{lc $key} = $value;
$txw->level($value) if($key =~ /wai/i);
} elsif($cmd eq 'all body') {
push @all, {type => 'like', text => $text, label => $label};
} elsif($cmd eq 'all body not') {
push @all, {type => 'unlike', text => $text, label => $label};
} elsif($cmd eq 'except') {
push @{ $all[-1]->{except} }, $text;
} elsif($cmd eq 'body') {
$label ||= ".. embedded text ('$text') found for '$link'";
$Test->like($content,qr!$text!s, $label);
$Test->diag($content) if($content !~ m!$text!s && $config{'dump'});
} elsif($cmd eq 'body not') {
$label ||= ".. embedded text ('$text') not found for '$link'";
$Test->unlike($content,qr!$text!s, $label);
$Test->diag($content) if($content =~ m!$text!s && $config{'dump'});
} elsif($cmd eq 'form' && $type eq 'url') {
my ($fname,$ftype) = split('=',$text,2);
$ftype = undef unless($ftype =~ /^(num|name|id)$/);
my $ok = 0;
my $rs;
if($fname =~ /^\d+$/ && (!$ftype || $ftype eq 'num')) {
eval { $rs = $mech->form_number($fname) };
#$Test->diag("form_number: rs=$rs, [$@]");
if(!$@ && $rs) { $ok = 1; }
}
if(!$ok && (!$ftype || $ftype eq 'name')) {
eval { $rs = $mech->form_name($fname) };
#$Test->diag("form_name: rs=$rs, [$@]");
if(!$@ && $rs) { $ok = 1; }
}
if(!$ok && (!$ftype || $ftype eq 'id')) {
eval { $rs = $mech->form_id($fname) };
#$Test->diag("form_id: rs=$rs, [$@]");
if(!$@ && $rs) { $ok = 1; }
}
$Test->ok($ok,".. form '$fname' found");
} elsif($cmd eq 'input' && $type eq 'url') {
my ($key,$value) = split('=',$text,2);
if($text eq 'submit' || $key eq 'submit') {
$mech->submit();
if($mech->success()) {
$content = $mech->content();
$link = $mech->base();
if(my $result = _check_xhtml(\%config,'xml',$content)) {
$Test->is_num($result->{PASS},1,"XHTML validity check for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txv->errstr());
$Test->diag(Dumper($txv->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
}
}
if(my $result = _check_wai(\%config,$content)) {
$Test->is_num($result->{PASS},1,"Content passes basic WAI compliance checks for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txw->errstr());
$Test->diag(Dumper($txw->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
$Test->diag(Dumper($content)) if($config{ 'dump'} && $config{ 'dump'} == 2);
}
}
if(my $result = _check_critic(\%config,$content)) {
$Test->is_num($result->{PASS},1,"Content passes basic page critique checks for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txc->errstr());
$Test->diag(Dumper($txc->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
$Test->diag(Dumper($content)) if($config{ 'dump'} && $config{ 'dump'} == 2);
}
}
} else {
$content = '';
}
} else {
$mech->field($key,$value);
}
} elsif($cmd eq 'file') {
$type = $cmd;
$link = $text;
if(my $result = _check_xhtml(\%config,$type,$link)) {
$Test->is_num($result->{PASS},1,"XHTML validity check for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txv->errstr());
$Test->diag(Dumper($txv->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
}
}
$content = $txv->content();
$label ||= "Got FILE '$link'";
$Test->ok($content,$label);
if(my $result = _check_wai(\%config,$content)) {
$Test->is_num($result->{PASS},1,"Content passes basic WAI compliance checks for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txw->errstr());
$Test->diag(Dumper($txw->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
$Test->diag(Dumper($content)) if($config{ 'dump'} && $config{ 'dump'} == 2);
}
}
if(my $result = _check_critic(\%config,$content)) {
$Test->is_num($result->{PASS},1,"Content passes basic page critique checks for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txc->errstr());
$Test->diag(Dumper($txc->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
$Test->diag(Dumper($content)) if($config{ 'dump'} && $config{ 'dump'} == 2);
}
}
for my $all (@all) {
my $ignore = 0;
for my $except (@{ $all->{except} }) {
next unless($link =~ /$except/);
$ignore = 1;
}
if($all->{type} eq 'like') {
$label = $all->{label} || ".. embedded text ('$all->{text}') found for '$link'";
next if($ignore);
$Test->like($content,qr!$all->{text}!, $label);
$Test->diag($content) if($content !~ m!$all->{text}! && $config{'dump'});
} else {
$label = $all->{label} || ".. embedded text ('$all->{text}') not found for '$link'";
next if($ignore);
$Test->unlike($content,qr!$all->{text}!, $label);
$Test->diag($content) if($content =~ m!$all->{text}! && $config{'dump'});
}
}
} elsif($cmd eq 'url') {
$type = $cmd;
$link = $text;
if(my $result = _check_xhtml(\%config,$type,$link)) {
$Test->is_num($result->{PASS},1,"XHTML validity check for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txv->errstr());
$Test->diag(Dumper($txv->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
}
}
$content = $txv->content();
$label ||= "Got URL '$link'";
$Test->ok($content,$label);
if(my $result = _check_wai(\%config,$content)) {
$Test->is_num($result->{PASS},1,"Content passes basic WAI compliance checks for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txw->errstr());
$Test->diag(Dumper($txw->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
$Test->diag(Dumper($content)) if($config{ 'dump'} && $config{ 'dump'} == 2);
}
}
if(my $result = _check_critic(\%config,$content)) {
$Test->is_num($result->{PASS},1,"Content passes basic page critique checks for '$link'");
if($result->{PASS} != 1) {
$Test->diag($txc->errstr());
$Test->diag(Dumper($txc->errors())) if($config{ 'dump'});
$Test->diag(Dumper($result)) if($config{ 'dump'});
$Test->diag(Dumper($content)) if($config{ 'dump'} && $config{ 'dump'} == 2);
}
}
for my $all (@all) {
my $ignore = 0;
for my $except (@{ $all->{except} }) {
next unless($link =~ /$except/);
$ignore = 1;
}
if($all->{type} eq 'like') {
$label = $all->{label} || ".. embedded text ('$all->{text}') found for '$link'";
next if($ignore);
$Test->like($content,qr!$all->{text}!, $label);
$Test->diag($content) if($content !~ m!$all->{text}! && $config{'dump'});
} else {
$label = $all->{label} || ".. embedded text ('$all->{text}') not found for '$link'";
next if($ignore);
$Test->unlike($content,qr!$all->{text}!, $label);
$Test->diag($content) if($content =~ m!$all->{text}! && $config{'dump'});
}
}
}
}
$fh->close;
}
sub _check_xhtml {
my ($config,$type,$link) = @_;
if($config->{xhtml}) {
$txv->clear();
if($type eq 'file') { $txv->process_file($link); }
elsif($type eq 'url') { $txv->process_link($link); }
elsif($type eq 'xml') { $txv->process_xml($link); }
return $txv->process_results();
} else {
if($type eq 'file') { $txv->retrieve_file($link); }
elsif($type eq 'url') { $txv->retrieve_url($link); }
}
return;
}
sub _check_wai {
my ($config,$content) = @_;
return unless($config->{wai});
$txw->clear();
$txw->validate($content);
return $txw->results();
}
sub _check_critic {
my ($config,$content) = @_;
return unless($config->{critic});
$txc->clear();
$txc->validate($content);
return $txc->results();
}
sub setlog {
my %hash = @_;
$txv->logfile($hash{logfile}) if($hash{logfile});
$txv->logclean($hash{logclean}) if(defined $hash{logclean});
$txw->logfile($hash{logfile}) if($hash{logfile});
$txw->logclean($hash{logclean}) if(defined $hash{logclean});
}
1;
__END__
=head1 FUNCTIONS
=head2 runtests(FILE)
Runs the tests contained within FILE. The entries in FILE define how the tests
are performed, and on what.
A simple file might look like:
#,# Configuration,
config,xhtml=1,
url,http://mysite/index.html,Test My Page
Where each field on the comma separated line represent 'cmd', 'text' and
'label'. Valid 'cmd' values are:
# - comment line, ignores the line
config - set configuration value (see below)
all body - test that 'text' exists in body content of all urls.
all body not - test that 'text' does not exist in body content of all urls.
url - test single url
body - test that 'text' exists in body content of the previous url.
body not - test that 'text' does not exist in body content of the
previous url.
form - name and type of form to use with subsequent input commands,
when more than one form exists in the page.
input - form fill, use as 'fieldname=xxx', with 'submit' as the last
input to submit the form.
The 'label' is used with the tests, and if left blank will be automatically
generated.
=head2 setlog(HASH)
If required will record a test run to a log file. If you do not wish to record
multiple runs, set 'logclean => 1' and log file will be recreated each time.
Otherwise all results are appended to the named log file.
Test::XHTML::setlog( logfile => './test.log', logclean => 1 );
=head1 CONFIGURATION
=head2 Options
There are currently 4 configuration options available, which can be listed in
your test file as follows:
#,# Configuration,
config,xhtml=1,
config,wai=1,
config,critic=1,
config,dump=1,
=head2 XHTML tests
Enable DTD valiadtion tests.
=head2 WAI WCAG v2.0 tests
Enable WAI WCAG v2.0 tests. Values can be set to represent the level of
compliance required.
config,wai=1, # Level A compliance
config,wai=2, # Level AA compliance
config,wai=3, # Level AAA compliance (not currently available)
=head2 Critique tests
Enable tests for some recommended Best Practices.
=head2 Dumping content
Where errors occur, it may be useful to obtain the page content to diagnose
problems. Enabling this option will produce the content as disanostics.
=head1 NOTES
=head2 Test::XHTML::Valid & xhtml-valid
The underlying package that provides the DTD validation framework, is only used
sparingly by Test::XHTML. Many more methods to test websites (both remote and
local) are supported, and can be accessed via the xhtml-valid script that
accompanies this distribution.
See script documentation and L for further details.
=head2 Internet Access
In some instances XML::LibXML may require internet access to obtain all the
necessary W3C and DTD specifications as denoted in the web pages you are
attempting to validate. However, for some more common specifications, for
HTML4 and XHTML1, this distribution pre-loads the XML Catalog to avoid making
calls across the internet.
=head1 BUGS, PATCHES & FIXES
There are no known bugs at the time of this release. However, if you spot a
bug or are experiencing difficulties, that is not explained within the POD
documentation, please send bug reports and patches to barbie@cpan.org.
Fixes are dependent upon their severity and my availability. Should a fix not
be forthcoming, please feel free to (politely) remind me.
=head1 SEE ALSO
L,
L
=head1 AUTHOR
Barbie,
for Miss Barbell Productions .
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2008-2013 Barbie for Miss Barbell Productions.
This distribution is free software; you can redistribute it and/or
modify it under the Artistic Licence v2.
=cut