File Coverage

blib/lib/Test/HTML/Content.pm
Criterion Covered Total %
statement 158 270 58.5
branch 32 90 35.5
condition 1 3 33.3
subroutine 46 65 70.7
pod 0 22 0.0
total 237 450 52.6


line stmt bran cond sub pod time code
1             package Test::HTML::Content;
2              
3             require 5.005_62;
4 21     21   193176 use strict;
  21         58  
  21         956  
5 21     21   130 use File::Spec;
  21         49  
  21         721  
6 21     21   141 use Carp qw(carp croak);
  21         41  
  21         3626  
7              
8 21     21   28593 use HTML::TokeParser;
  21         355427  
  21         1240  
9              
10             # we want to stay compatible to 5.5 and use warnings if
11             # we can
12 21     21   116 eval 'use warnings' if $] >= 5.006;
  21         47  
  21         353  
13 21     21   206 use Test::Builder;
  21         50  
  21         979  
14             require Exporter;
15              
16 21     21   117 use vars qw/@ISA @EXPORT_OK @EXPORT $VERSION $can_xpath/;
  21         49  
  21         5189  
17              
18             @ISA = qw(Exporter);
19              
20 21     21   121 use vars qw( $tidy );
  21         54  
  21         1656  
21              
22             # DONE:
23             # * use Test::Builder;
24             # * Add comment_ok() method
25             # * Allow RE instead of plain strings in the functions (for tag attributes and comments)
26             # * Create a function to check the DOCTYPE and other directives
27             # * Have a better way to diagnose ignored candidates in tag_ok(), tag_count
28             # and no_tag() in case a test fails
29              
30             @EXPORT = qw(
31             link_ok no_link link_count
32             tag_ok no_tag tag_count
33             comment_ok no_comment comment_count
34             has_declaration no_declaration
35             text_ok no_text text_count
36             title_ok no_title
37             xpath_ok no_xpath xpath_count
38             );
39              
40             $VERSION = '0.09';
41              
42             my $Test = Test::Builder->new;
43              
44 21     21   1486 use vars qw($HTML_PARSER_StripsTags $parsing_method);
  21         121  
  21         128066  
45             $parsing_method = 'parse_html_string';
46              
47             # Cribbed from the Test::Builder synopsis
48             sub import {
49 23     23   654 my($self) = shift;
50 23         92 my $pack = caller;
51 23         147 $Test->exported_to($pack);
52 23         402 $Test->plan(@_);
53 23         6265 $self->export_to_level(1, $self, @EXPORT);
54             }
55              
56             sub __dwim_compare {
57             # Do the Right Thing (Perl 6 style) with the RHS being a Regex or a string
58 172     172   11673 my ($target,$template) = @_;
59 172 100       411 if (ref $template) { # supposedly a Regexp, but possibly blessed, so no eq comparision
60 77         969 return ($target =~ $template )
61             } else {
62 95         701 return $target eq $template;
63             };
64             };
65              
66             sub __node_content {
67 0     0   0 my $node = shift;
68 0 0       0 if ($can_xpath eq 'XML::XPath') { return XML::XPath::XMLParser::as_string($node) };
  0         0  
69 0 0       0 if ($can_xpath eq 'XML::LibXML') { return $node->toString };
  0         0  
70             };
71              
72             sub __text_content {
73 0     0   0 my $node = shift;
74 0 0       0 if ($can_xpath eq 'XML::XPath') { return $node->string_value };
  0         0  
75 0 0       0 if ($can_xpath eq 'XML::LibXML') { return $node->textContent };
  0         0  
76             }
77              
78             sub __match_comment {
79             my ($text,$template) = @_;
80             $text =~ s/^$/$1/sm unless $HTML_PARSER_StripsTags;
81             unless (ref $template eq "Regexp") {
82             $text =~ s/^\s*(.*?)\s*$/$1/;
83             $template =~ s/^\s*(.*?)\s*$/$1/;
84             };
85             return __dwim_compare($text, $template);
86             };
87              
88             sub __count_comments {
89             my ($HTML,$comment) = @_;
90             my $tree;
91             $tree = __get_node_tree($HTML,'//comment()');
92             return (undef,undef) unless ($tree);
93              
94             my $result = 0;
95             my @seen;
96              
97             foreach my $node ($tree->get_nodelist) {
98             my $content = __node_content($node);
99             $content =~ s/\A\Z/$1/gsm;
100             push @seen, $content;
101             $result++ if __match_comment($content,$comment);
102             };
103              
104             $_ = "" for @seen;
105             return ($result, \@seen);
106             };
107              
108             sub __output_diag {
109 37     37   75 my ($cond,$match,$descr,$kind,$name,$seen) = @_;
110              
111 37         59 local $Test::Builder::Level = $Test::Builder::Level + 2;
112              
113 37 100       124 unless ($Test->ok($cond,$name)) {
114 7 100       3611 if (@$seen) {
115 4         20 $Test->diag( "Saw '$_'" ) for @$seen;
116             } else {
117 3         12 $Test->diag( "No $kind found at all" );
118             };
119 7         864 $Test->diag( "Expected $descr like '$match'" );
120             };
121             };
122              
123             sub __invalid_html {
124 0     0   0 my ($HTML,$name) = @_;
125 0 0       0 carp "No test name given" unless $name;
126 0         0 $Test->ok(0,$name);
127 0         0 $Test->diag( "Invalid HTML:");
128 0         0 $Test->diag($HTML);
129             };
130              
131             sub __output_comment {
132 20     20   37 my ($check,$expectation,$HTML,$comment,$name) = @_;
133 20         63 my ($result,$seen) = __count_comments($HTML,$comment);
134              
135 20 50       45 if (defined $result) {
136 20         46 $result = $check->($result);
137 20         1149 __output_diag($result,$comment,$expectation,"comment",$name,$seen);
138             } else {
139 0         0 local $Test::Builder::Level = $Test::Builder::Level +2;
140 0         0 __invalid_html($HTML,$name);
141             };
142              
143 20         27889 $result;
144             };
145              
146             sub comment_ok {
147 10     10 0 1555 my ($HTML,$comment,$name) = @_;
148 10     10   64 __output_comment(sub{shift},"at least one comment",$HTML,$comment,$name);
  10         18  
149             };
150              
151             sub no_comment {
152 5     5 0 874 my ($HTML,$comment,$name) = @_;
153 5     5   24 __output_comment(sub{shift == 0},"no comment",$HTML,$comment,$name);
  5         10  
154             };
155              
156             sub comment_count {
157 5     5 0 1787 my ($HTML,$comment,$count,$name) = @_;
158 5     5   32 __output_comment(sub{shift == $count},"exactly $count comments",$HTML,$comment,$name);
  5         12  
159             };
160              
161             sub __match_text {
162             my ($text,$template) = @_;
163             unless (ref $template eq "Regexp") {
164             $text =~ s/^\s*(.*?)\s*$/$1/;
165             $template =~ s/^\s*(.*?)\s*$/$1/;
166             };
167             return __dwim_compare($text, $template);
168             };
169              
170             sub __count_text {
171             my ($HTML,$text) = @_;
172             my $tree = __get_node_tree($HTML,'//text()');
173             return (undef,undef) unless $tree;
174              
175             my $result = 0;
176             my @seen;
177              
178             foreach my $node ($tree->get_nodelist) {
179             my $content = __node_content($node);
180             push @seen, $content
181             unless $content =~ /\A\r?\n?\Z/sm;
182             $result++ if __match_text($content,$text);
183             };
184              
185             return ($result, \@seen);
186             };
187              
188             sub __output_text {
189 13     13   27 my ($check,$expectation,$HTML,$text,$name) = @_;
190 13         45 my ($result,$seen) = __count_text($HTML,$text);
191              
192 13 50       29 if (defined $result) {
193 13         21 local $Test::Builder::Level = $Test::Builder::Level;
194 13         32 $result = $check->($result);
195 13         32 __output_diag($result,$text,$expectation,"text",$name,$seen);
196             } else {
197 0         0 local $Test::Builder::Level = $Test::Builder::Level +2;
198 0         0 __invalid_html($HTML,$name);
199             };
200              
201 13         7090 $result;
202             };
203              
204             sub text_ok {
205 6     6 0 1255 my ($HTML,$text,$name) = @_;
206 6     6   33 __output_text(sub{shift > 0}, "at least one text element",$HTML,$text,$name);
  6         14  
207             };
208              
209             sub no_text {
210 3     3 0 28 my ($HTML,$text,$name) = @_;
211 3     3   15 __output_text(sub{shift == 0}, "no text elements",$HTML,$text,$name);
  3         148  
212             };
213              
214             sub text_count {
215 4     4 0 41 my ($HTML,$text,$count,$name) = @_;
216 4     4   91 __output_text(sub{shift == $count}, "exactly $count elements",$HTML,$text,$name);
  4         7  
217             };
218              
219             sub __match {
220             my ($attrs,$currattr,$key) = @_;
221             my $result = 1;
222              
223             if (exists $currattr->{$key}) {
224             if (! defined $attrs->{$key}) {
225             $result = 0; # We don't want to see this attribute here
226             } else {
227             $result = 0 unless __dwim_compare($currattr->{$key}, $attrs->{$key});
228             };
229             } else {
230             if (! defined $attrs->{$key}) {
231             $result = 0 if (exists $currattr->{$key});
232             } else {
233             $result = 0;
234             };
235             };
236             return $result;
237             };
238              
239             sub __get_node_tree {
240 0     0   0 my ($HTML,$query) = @_;
241              
242 0 0       0 croak "No HTML given" unless defined $HTML;
243 0 0       0 croak "No query given" unless defined $query;
244              
245 0         0 my ($tree,$find,$result);
246 0 0       0 if ($HTML !~ m!\A\s*\Z!ms) {
247 0         0 eval {
248 0         0 require XML::LibXML; XML::LibXML->import;
  0         0  
249 0         0 my $parser = XML::LibXML->new();
250 0         0 $parser->recover(1);
251 0         0 $tree = $parser->$parsing_method($HTML);
252 0         0 $find = 'findnodes';
253 0         0 $HTML_PARSER_StripsTags = 1;
254             };
255 0 0       0 unless ($tree) {
256 0         0 eval {
257 0         0 require XML::XPath; XML::XPath->import;
  0         0  
258 0         0 require XML::Parser;
259              
260 0         0 my $p = XML::Parser->new( ErrorContext => 2, ParseParamEnt => 0, NoLWP => 1 );
261 0         0 $tree = XML::XPath->new( parser => $p, xml => $HTML );
262 0         0 $find = 'find';
263             };
264             };
265 0 0       0 undef $tree if $@;
266              
267 0 0       0 if ($tree) {
268 0         0 eval {
269 0         0 $result = $tree->$find($query);
270 0 0       0 unless ($result) {
271 0         0 $result = {};
272 0         0 bless $result, 'Test::HTML::Content::EmptyXPathResult';
273             };
274             };
275 0 0       0 warn $@ if $@;
276             };
277             } else { };
278 0         0 return $result;
279             };
280              
281             sub __get_node_content {
282 0     0   0 my ($node,$name) = @_;
283              
284 0 0       0 if ($name eq '_content') {
285 0         0 return __text_content( $node )
286             # return $node->textContent()
287             } else {
288 0         0 return $node->getAttribute($name)
289             };
290             };
291              
292             sub __build_xpath_query {
293 4     4   1921 my ($query,$attrref) = @_;
294 4         8 my @postvalidation;
295 4 50       13 if ($attrref) {
296 4         5 my @query;
297 4         21 for (sort keys %$attrref) {
298 8         12 my $name = $_;
299 8         12 my $value = $attrref->{$name};
300 8         11 my $xpath_name = '@' . $name;
301 8 50       22 if ($name eq '_content') { $xpath_name = "text()" };
  0         0  
302 8 100       26 if (! defined $value) {
    100          
303 2         7 push @query, "not($xpath_name)"
304             } elsif ((ref $value) ne 'Regexp') {
305 3         10 push @query, "$xpath_name = \"$value\"";
306             push @postvalidation, sub {
307 0     0   0 return __get_node_content( shift,$name ) eq $value
308 3         15 };
309             } else {
310 3         5 push @query, "$xpath_name";
311             push @postvalidation, sub {
312 0     0   0 return __get_node_content( shift,$name ) =~ $value
313 3         15 };
314             };
315             };
316 4 50       16 $query .= "[" . join( " and ", map {"$_"} @query ) . "]"
  8         27  
317             if @query;
318             };
319             my $postvalidation = sub {
320 0     0   0 my $node = shift;
321 0         0 my $test;
322 0         0 for $test (@postvalidation) {
323 0 0       0 return () unless $test->($node);
324             };
325 0         0 return 1;
326 4         14 };
327 4         14 ($query,$postvalidation);
328             };
329              
330             sub __count_tags {
331             my ($HTML,$tag,$attrref) = @_;
332             $attrref = {} unless defined $attrref;
333              
334             my $fallback = lc "//$tag";
335             my ($query,$valid) = __build_xpath_query( lc "//$tag", $attrref );
336             my $tree = __get_node_tree($HTML,$query);
337             return (undef,undef) unless $tree;
338              
339             my @found = grep { $valid->($_) } ($tree->get_nodelist);
340              
341             # Collect the nodes we did see for later reference :
342             my @seen;
343             foreach my $node (__get_node_tree($HTML,$fallback)->get_nodelist) {
344             push @seen, __node_content($node);
345             };
346             return scalar(@found),\@seen;
347             };
348              
349             sub __tag_diag {
350 5     5   12 my ($tag,$num,$attrs,$found) = @_;
351 5         11 my $phrase = "Expected to find $num <$tag> tag(s)";
352 5 50       17 $phrase .= " matching" if (scalar keys %$attrs > 0);
353 5         13 $Test->diag($phrase);
354             $Test->diag(" $_ = " . (defined $attrs->{$_} ? $attrs->{$_} : ''))
355 5 50       377 for sort keys %$attrs;
356 5 100       356 if (@$found) {
357 4         12 $Test->diag("Got");
358 4         268 $Test->diag(" " . $_) for @$found;
359             } else {
360 1         4 $Test->diag("Got none");
361             };
362             };
363              
364             sub __output_tag {
365 51     51   104 my ($check,$expectation,$HTML,$tag,$attrref,$name) = @_;
366 51 100       182 ($attrref,$name) = ({},$attrref)
367             unless defined $name;
368 51 100       114 $attrref = {}
369             unless defined $attrref;
370 51 50       153 croak "$attrref dosen't look like a hash reference for the attributes"
371             unless ref $attrref eq 'HASH';
372 51         160 my ($currcount,$seen) = __count_tags($HTML,$tag,$attrref);
373 51         78 my $result;
374 51 50       106 if (defined $currcount) {
375 51 100       103 if ($currcount eq 'skip') {
376 6         28 $Test->skip($seen);
377             } else {
378 45         69 local $Test::Builder::Level = $Test::Builder::Level +1;
379 45         97 $result = $check->($currcount);
380 45 100       154 unless ($Test->ok($result, $name)) {
381 5         2593 __tag_diag($tag,$expectation,$attrref,$seen) ;
382             };
383             };
384             } else {
385 0         0 local $Test::Builder::Level = $Test::Builder::Level +2;
386 0         0 __invalid_html($HTML,$name);
387             };
388              
389 51         20539 $result;
390             };
391              
392             sub tag_count {
393 14     14 0 111 my ($HTML,$tag,$attrref,$count,$name) = @_;
394 14     14   86 __output_tag(sub { shift == $count }, "exactly $count",$HTML,$tag,$attrref,$name);
  14         35  
395             };
396              
397             sub tag_ok {
398 25     25 0 166 my ($HTML,$tag,$attrref,$name) = @_;
399 25     21   208 __output_tag(sub { shift > 0 }, "at least one",$HTML,$tag,$attrref,$name);
  21         47  
400             };
401              
402             sub no_tag {
403 12     12 0 84 my ($HTML,$tag,$attrref,$name) = @_;
404 12     10   65 __output_tag(sub { shift == 0 }, "no",$HTML,$tag,$attrref,$name);
  10         25  
405             };
406              
407             sub link_count {
408 3     3 0 2075 my ($HTML,$link,$count,$name) = @_;
409 3         6 local $Test::Builder::Level = 2;
410 3         14 return tag_count($HTML,"a",{href => $link},$count,$name);
411             };
412              
413             sub link_ok {
414 7     7 0 3898 my ($HTML,$link,$name) = (@_);
415 7         17 local $Test::Builder::Level = 2;
416 7         36 return tag_ok($HTML,'a',{ href => $link },$name);
417             };
418              
419             sub no_link {
420 3     3 0 944 my ($HTML,$link,$name) = (@_);
421 3         6 local $Test::Builder::Level = 2;
422 3         15 return no_tag($HTML,'a',{ href => $link },$name);
423             };
424              
425             sub title_ok {
426 4     4 0 1878 my ($HTML,$title,$name) = @_;
427 4         9 local $Test::Builder::Level = 2;
428 4         19 return tag_ok($HTML,"title",{_content => $title},$name);
429             };
430              
431             sub no_title {
432 2     2 0 13590 my ($HTML,$title,$name) = (@_);
433 2         8 local $Test::Builder::Level = 2;
434 2         14 return no_tag($HTML,'title',{ _content => $title },$name);
435             };
436              
437             sub __match_declaration {
438             my ($text,$template) = @_;
439             $text =~ s/^$/$1/ unless $HTML_PARSER_StripsTags;
440             unless (ref $template eq "Regexp") {
441             $text =~ s/^\s*(.*?)\s*$/$1/;
442             $template =~ s/^\s*(.*?)\s*$/$1/;
443             };
444             return __dwim_compare($text, $template);
445             };
446              
447             sub __count_declarations {
448             my ($HTML,$doctype) = @_;
449             my $result = 0;
450             my $seen = [];
451              
452             my $p = HTML::TokeParser->new(\$HTML);
453             my $token;
454             while ($token = $p->get_token) {
455             my ($type,$text) = @$token;
456             if ($type eq "D") {
457             push @$seen, $text;
458             $result++ if __match_declaration($text,$doctype);
459             };
460             };
461              
462             return $result, $seen;
463             };
464              
465             sub has_declaration {
466 3     3 0 550 my ($HTML,$declaration,$name) = @_;
467 3         12 my ($result,$seen) = __count_declarations($HTML,$declaration);
468              
469 3 50       9 if (defined $result) {
470 3         10 __output_diag($result == 1,$declaration,"exactly one declaration","declaration",$name,$seen);
471             } else {
472 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
473 0         0 __invalid_html($HTML,$name);
474             };
475              
476 3         3661 $result;
477             };
478              
479             sub no_declaration {
480 1     1 0 12 my ($HTML,$declaration,$name) = @_;
481 1         4 my ($result,$seen) = __count_declarations($HTML,$declaration);
482              
483 1 50       5 if (defined $result) {
484 1         5 __output_diag($result == 0,$declaration,"no declaration","declaration",$name,$seen);
485             } else {
486 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
487 0         0 __invalid_html($HTML,$name);
488             };
489              
490 1         327 $result;
491             };
492              
493             sub __count_xpath {
494 0     0   0 my ($HTML,$query,$fallback) = @_;
495              
496 0 0       0 $fallback = $query unless defined $fallback;
497 0         0 my $tree = __get_node_tree($HTML,$query);
498 0 0       0 return (undef,undef) unless $tree;
499              
500 0         0 my @found = ($tree->get_nodelist);
501              
502             # Collect the nodes we did see for later reference :
503 0         0 my @seen;
504 0         0 foreach my $node (__get_node_tree($HTML,$fallback)->get_nodelist) {
505 0         0 push @seen, __node_content($node);
506             };
507 0         0 return scalar(@found),\@seen;
508             };
509              
510             sub __xpath_diag {
511 0     0   0 my ($query,$num,$found) = @_;
512 0         0 my $phrase = "Expected to find $num nodes matching on '$query'";
513 0 0       0 if (@$found) {
514 0         0 $Test->diag("Got");
515 0         0 $Test->diag(" $_") for @$found;
516             } else {
517 0         0 $Test->diag("Got none");
518             };
519             };
520              
521             sub __output_xpath {
522 0     0   0 my ($check,$expectation,$HTML,$query,$fallback,$name) = @_;
523 0 0       0 ($fallback,$name) = ($query,$fallback) unless $name;
524 0         0 my ($currcount,$seen) = __count_xpath($HTML,$query,$fallback);
525 0         0 my $result;
526 0 0       0 if (defined $currcount) {
527 0 0       0 if ($currcount eq 'skip') {
528 0         0 $Test->skip($seen);
529             } else {
530 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
531 0         0 $result = $check->($currcount);
532 0 0       0 unless ($Test->ok($result, $name)) {
533 0         0 __xpath_diag($query,$expectation,$seen) ;
534             };
535             };
536             } else {
537 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
538 0         0 __invalid_html($HTML,$name);
539             };
540              
541 0         0 $result;
542             };
543              
544             sub xpath_count {
545 0     0 0 0 my ($HTML,$query,$count,$fallback,$name) = @_;
546 0     0   0 __output_xpath( sub {shift == $count},"exactly $count",$HTML,$query,$fallback,$name);
  0         0  
547             };
548              
549             sub xpath_ok {
550 0     0 0 0 my ($HTML,$query,$fallback,$name) = @_;
551 0     0   0 __output_xpath( sub{shift > 0},"at least one",$HTML,$query,$fallback,$name);
  0         0  
552             };
553              
554             sub no_xpath {
555 0     0 0 0 my ($HTML,$query,$fallback,$name) = @_;
556 0     0   0 __output_xpath( sub{shift == 0},"no",$HTML,$query,$fallback,$name);
  0         0  
557             };
558              
559             sub install_xpath {
560 21     21 0 8701 require XML::XPath;
561 0         0 XML::XPath->import();
562 0 0       0 die "Need XML::XPath 1.13 or higher"
563             unless $XML::XPath::VERSION >= 1.13;
564 0         0 $can_xpath = 'XML::XPath';
565             };
566              
567             sub install_libxml {
568 21     21 0 240 local $^W;
569 21         8808 require XML::LibXML;
570 0         0 XML::LibXML->import();
571 0         0 $can_xpath = 'XML::LibXML';
572             };
573              
574             # And install our plain handlers if we have to :
575             sub install_pureperl {
576 32     32 0 30919 require Test::HTML::Content::NoXPath;
577 32         218 Test::HTML::Content::NoXPath->import;
578             };
579              
580             BEGIN {
581             # Load the XML-variant if our prerequisites are there :
582 21         76 eval { install_libxml }
583 21 50 33 21   265 or eval { install_xpath }
  21         1085  
584             or install_pureperl;
585             };
586              
587             {
588             package Test::HTML::Content::EmptyXPathResult;
589 0     0     sub size { 0 };
590 0     0     sub get_nodelist { () };
591             };
592              
593             1;
594              
595             __END__