File Coverage

blib/lib/Test/HTML/Content.pm
Criterion Covered Total %
statement 161 276 58.3
branch 32 90 35.5
condition 1 3 33.3
subroutine 46 65 70.7
pod 0 22 0.0
total 240 456 52.6


line stmt bran cond sub pod time code
1             package Test::HTML::Content;
2              
3             require 5.005_62;
4 18     18   277685 use strict;
  18         65  
  18         565  
5 18     18   98 use File::Spec;
  18         37  
  18         522  
6 18     18   107 use Carp qw(carp croak);
  18         41  
  18         1590  
7              
8 18     18   9259 use HTML::TokeParser;
  18         183654  
  18         848  
9              
10             # we want to stay compatible to 5.5 and use warnings if
11             # we can
12 18     18   117 eval 'use warnings' if $] >= 5.006;
  18         37  
  18         321  
13 18     18   133 use Test::Builder;
  18         45  
  18         706  
14             require Exporter;
15              
16 18     18   113 use vars qw/@ISA @EXPORT_OK @EXPORT $VERSION $can_xpath/;
  18         36  
  18         1317  
17              
18             @ISA = qw(Exporter);
19              
20 18     18   136 use vars qw( $tidy );
  18         81  
  18         1247  
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.12';
41              
42             my $Test = Test::Builder->new;
43              
44 18     18   112 use vars qw($HTML_PARSER_StripsTags $parsing_method);
  18         48  
  18         83476  
45             $parsing_method = 'parse_html_string';
46              
47             # Cribbed from the Test::Builder synopsis
48             sub import {
49 20     20   649 my($self) = shift;
50 20         46 my $pack = caller;
51 20         126 $Test->exported_to($pack);
52 20         271 $Test->plan(@_);
53 20         3958 $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 167     167   8165 my ($target,$template) = @_;
59 167 100       334 if (ref $template) { # supposedly a Regexp, but possibly blessed, so no eq comparision
60 77         754 return ($target =~ $template )
61             } else {
62 90         388 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   91 my ($cond,$match,$descr,$kind,$name,$seen) = @_;
110              
111 37         60 local $Test::Builder::Level = $Test::Builder::Level + 2;
112              
113 37 100       109 unless ($Test->ok($cond,$name)) {
114 7 100       7150 if (@$seen) {
115 4         16 $Test->diag( "Saw '$_'" ) for @$seen;
116             } else {
117 3         13 $Test->diag( "No $kind found at all" );
118             };
119 7         2870 $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   48 my ($check,$expectation,$HTML,$comment,$name) = @_;
133 20         54 my ($result,$seen) = __count_comments($HTML,$comment);
134              
135 20 50       47 if (defined $result) {
136 20         37 $result = $check->($result);
137 20         75 __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         5452 $result;
144             };
145              
146             sub comment_ok {
147 10     10 0 4988 my ($HTML,$comment,$name) = @_;
148 10     10   77 __output_comment(sub{shift},"at least one comment",$HTML,$comment,$name);
  10         20  
149             };
150              
151             sub no_comment {
152 5     5 0 2817 my ($HTML,$comment,$name) = @_;
153 5     5   23 __output_comment(sub{shift == 0},"no comment",$HTML,$comment,$name);
  5         14  
154             };
155              
156             sub comment_count {
157 5     5 0 5375 my ($HTML,$comment,$count,$name) = @_;
158 5     5   34 __output_comment(sub{shift == $count},"exactly $count comments",$HTML,$comment,$name);
  5         11  
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   32 my ($check,$expectation,$HTML,$text,$name) = @_;
190 13         38 my ($result,$seen) = __count_text($HTML,$text);
191              
192 13 50       32 if (defined $result) {
193 13         23 local $Test::Builder::Level = $Test::Builder::Level;
194 13         27 $result = $check->($result);
195 13         28 __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         4906 $result;
202             };
203              
204             sub text_ok {
205 6     6 0 3352 my ($HTML,$text,$name) = @_;
206 6     6   54 __output_text(sub{shift > 0}, "at least one text element",$HTML,$text,$name);
  6         15  
207             };
208              
209             sub no_text {
210 3     3 0 35 my ($HTML,$text,$name) = @_;
211 3     3   15 __output_text(sub{shift == 0}, "no text elements",$HTML,$text,$name);
  3         6  
212             };
213              
214             sub text_count {
215 4     4 0 68 my ($HTML,$text,$count,$name) = @_;
216 4     4   22 __output_text(sub{shift == $count}, "exactly $count elements",$HTML,$text,$name);
  4         9  
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   1834 my ($query,$attrref) = @_;
294 4         8 my @postvalidation;
295 4 50       35 if ($attrref) {
296 4         6 my @query;
297 4         26 for (sort keys %$attrref) {
298 8         12 my $name = $_;
299 8         14 my $value = $attrref->{$name};
300 8         13 my $xpath_name = '@' . $name;
301 8 50       18 if ($name eq '_content') { $xpath_name = "text()" };
  0         0  
302 8 100       21 if (! defined $value) {
    100          
303 2         6 push @query, "not($xpath_name)"
304             } elsif ((ref $value) ne 'Regexp') {
305 3         9 push @query, "$xpath_name = \"$value\"";
306             push @postvalidation, sub {
307 0     0   0 return __get_node_content( shift,$name ) eq $value
308 3         13 };
309             } else {
310 3         7 push @query, "$xpath_name";
311             push @postvalidation, sub {
312 0     0   0 return __get_node_content( shift,$name ) =~ $value
313 3         13 };
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         13 };
327 4         16 ($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   13 my ($tag,$num,$attrs,$found) = @_;
351 5         13 my $phrase = "Expected to find $num <$tag> tag(s)";
352 5 50       15 $phrase .= " matching" if (scalar keys %$attrs > 0);
353 5         14 $Test->diag($phrase);
354             $Test->diag(" $_ = " . (defined $attrs->{$_} ? $attrs->{$_} : ''))
355 5 50       1255 for sort keys %$attrs;
356 5 100       1170 if (@$found) {
357 4         18 $Test->diag("Got");
358 4         917 for my $tag (@$found) {
359 10         1444 my $vis = "$tag";
360 10         21 $vis =~ s!\s*/>\s*$!/>!; # canonicalize between XML::Parser and XML::LibXML
361 10         29 $Test->diag(" " . $vis);
362             };
363             } else {
364 1         4 $Test->diag("Got none");
365             };
366             };
367              
368             sub __output_tag {
369 48     48   117 my ($check,$expectation,$HTML,$tag,$attrref,$name) = @_;
370 48 100       108 ($attrref,$name) = ({},$attrref)
371             unless defined $name;
372 48 100       94 $attrref = {}
373             unless defined $attrref;
374 48 50       131 croak "$attrref dosen't look like a hash reference for the attributes"
375             unless ref $attrref eq 'HASH';
376 48         124 my ($currcount,$seen) = __count_tags($HTML,$tag,$attrref);
377 48         78 my $result;
378 48 50       126 if (defined $currcount) {
379 48 100       96 if ($currcount eq 'skip') {
380 6         17 $Test->skip($seen);
381             } else {
382 42         71 local $Test::Builder::Level = $Test::Builder::Level +1;
383 42         87 $result = $check->($currcount);
384 42 100       129 unless ($Test->ok($result, $name)) {
385 5         4917 __tag_diag($tag,$expectation,$attrref,$seen) ;
386             };
387             };
388             } else {
389 0         0 local $Test::Builder::Level = $Test::Builder::Level +2;
390 0         0 __invalid_html($HTML,$name);
391             };
392              
393 48         14341 $result;
394             };
395              
396             sub tag_count {
397 14     14 0 133 my ($HTML,$tag,$attrref,$count,$name) = @_;
398 14     14   73 __output_tag(sub { shift == $count }, "exactly $count",$HTML,$tag,$attrref,$name);
  14         39  
399             };
400              
401             sub tag_ok {
402 22     22 0 192 my ($HTML,$tag,$attrref,$name) = @_;
403 22     18   97 __output_tag(sub { shift > 0 }, "at least one",$HTML,$tag,$attrref,$name);
  18         42  
404             };
405              
406             sub no_tag {
407 12     12 0 97 my ($HTML,$tag,$attrref,$name) = @_;
408 12     10   51 __output_tag(sub { shift == 0 }, "no",$HTML,$tag,$attrref,$name);
  10         22  
409             };
410              
411             sub link_count {
412 3     3 0 5543 my ($HTML,$link,$count,$name) = @_;
413 3         7 local $Test::Builder::Level = 2;
414 3         22 return tag_count($HTML,"a",{href => $link},$count,$name);
415             };
416              
417             sub link_ok {
418 4     4 0 4671 my ($HTML,$link,$name) = (@_);
419 4         7 local $Test::Builder::Level = 2;
420 4         15 return tag_ok($HTML,'a',{ href => $link },$name);
421             };
422              
423             sub no_link {
424 3     3 0 2781 my ($HTML,$link,$name) = (@_);
425 3         6 local $Test::Builder::Level = 2;
426 3         11 return no_tag($HTML,'a',{ href => $link },$name);
427             };
428              
429             sub title_ok {
430 4     4 0 1802 my ($HTML,$title,$name) = @_;
431 4         7 local $Test::Builder::Level = 2;
432 4         13 return tag_ok($HTML,"title",{_content => $title},$name);
433             };
434              
435             sub no_title {
436 2     2 0 1050 my ($HTML,$title,$name) = (@_);
437 2         5 local $Test::Builder::Level = 2;
438 2         7 return no_tag($HTML,'title',{ _content => $title },$name);
439             };
440              
441             sub __match_declaration {
442             my ($text,$template) = @_;
443             $text =~ s/^$/$1/ unless $HTML_PARSER_StripsTags;
444             unless (ref $template eq "Regexp") {
445             $text =~ s/^\s*(.*?)\s*$/$1/;
446             $template =~ s/^\s*(.*?)\s*$/$1/;
447             };
448             return __dwim_compare($text, $template);
449             };
450              
451             sub __count_declarations {
452             my ($HTML,$doctype) = @_;
453             my $result = 0;
454             my $seen = [];
455              
456             my $p = HTML::TokeParser->new(\$HTML);
457             my $token;
458             while ($token = $p->get_token) {
459             my ($type,$text) = @$token;
460             if ($type eq "D") {
461             push @$seen, $text;
462             $result++ if __match_declaration($text,$doctype);
463             };
464             };
465              
466             return $result, $seen;
467             };
468              
469             sub has_declaration {
470 3     3 0 471 my ($HTML,$declaration,$name) = @_;
471 3         8 my ($result,$seen) = __count_declarations($HTML,$declaration);
472              
473 3 50       7 if (defined $result) {
474 3         7 __output_diag($result == 1,$declaration,"exactly one declaration","declaration",$name,$seen);
475             } else {
476 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
477 0         0 __invalid_html($HTML,$name);
478             };
479              
480 3         1207 $result;
481             };
482              
483             sub no_declaration {
484 1     1 0 11 my ($HTML,$declaration,$name) = @_;
485 1         3 my ($result,$seen) = __count_declarations($HTML,$declaration);
486              
487 1 50       4 if (defined $result) {
488 1         3 __output_diag($result == 0,$declaration,"no declaration","declaration",$name,$seen);
489             } else {
490 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
491 0         0 __invalid_html($HTML,$name);
492             };
493              
494 1         413 $result;
495             };
496              
497             sub __count_xpath {
498 0     0   0 my ($HTML,$query,$fallback) = @_;
499              
500 0 0       0 $fallback = $query unless defined $fallback;
501 0         0 my $tree = __get_node_tree($HTML,$query);
502 0 0       0 return (undef,undef) unless $tree;
503              
504 0         0 my @found = ($tree->get_nodelist);
505              
506             # Collect the nodes we did see for later reference :
507 0         0 my @seen;
508 0         0 foreach my $node (__get_node_tree($HTML,$fallback)->get_nodelist) {
509 0         0 push @seen, __node_content($node);
510             };
511 0         0 return scalar(@found),\@seen;
512             };
513              
514             sub __xpath_diag {
515 0     0   0 my ($query,$num,$found) = @_;
516 0         0 my $phrase = "Expected to find $num nodes matching on '$query'";
517 0 0       0 if (@$found) {
518 0         0 $Test->diag("Got");
519 0         0 for my $tag (@$found) {
520 0         0 my $vis = "$tag";
521 0         0 $vis =~ s!\s*/>$!/>!; # canonicalize between XML::Parser and XML::LibXML
522 0         0 $Test->diag(" $vis");
523             }
524             } else {
525 0         0 $Test->diag("Got none");
526             };
527             };
528              
529             sub __output_xpath {
530 0     0   0 my ($check,$expectation,$HTML,$query,$fallback,$name) = @_;
531 0 0       0 ($fallback,$name) = ($query,$fallback) unless $name;
532 0         0 my ($currcount,$seen) = __count_xpath($HTML,$query,$fallback);
533 0         0 my $result;
534 0 0       0 if (defined $currcount) {
535 0 0       0 if ($currcount eq 'skip') {
536 0         0 $Test->skip($seen);
537             } else {
538 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
539 0         0 $result = $check->($currcount);
540 0 0       0 unless ($Test->ok($result, $name)) {
541 0         0 __xpath_diag($query,$expectation,$seen) ;
542             };
543             };
544             } else {
545 0         0 local $Test::Builder::Level = $Test::Builder::Level +1;
546 0         0 __invalid_html($HTML,$name);
547             };
548              
549 0         0 $result;
550             };
551              
552             sub xpath_count {
553 0     0 0 0 my ($HTML,$query,$count,$fallback,$name) = @_;
554 0     0   0 __output_xpath( sub {shift == $count},"exactly $count",$HTML,$query,$fallback,$name);
  0         0  
555             };
556              
557             sub xpath_ok {
558 0     0 0 0 my ($HTML,$query,$fallback,$name) = @_;
559 0     0   0 __output_xpath( sub{shift > 0},"at least one",$HTML,$query,$fallback,$name);
  0         0  
560             };
561              
562             sub no_xpath {
563 0     0 0 0 my ($HTML,$query,$fallback,$name) = @_;
564 0     0   0 __output_xpath( sub{shift == 0},"no",$HTML,$query,$fallback,$name);
  0         0  
565             };
566              
567             sub install_xpath {
568 18     18 0 2329 require XML::XPath;
569 0         0 XML::XPath->import();
570 0 0       0 die "Need XML::XPath 1.13 or higher"
571             unless $XML::XPath::VERSION >= 1.13;
572 0         0 $can_xpath = 'XML::XPath';
573             };
574              
575             sub install_libxml {
576 18     18 0 125 local $^W;
577 18         2948 require XML::LibXML;
578 0         0 XML::LibXML->import();
579 0         0 $can_xpath = 'XML::LibXML';
580             };
581              
582             # And install our plain handlers if we have to :
583             sub install_pureperl {
584 29     29 0 15196 require Test::HTML::Content::NoXPath;
585 29         169 Test::HTML::Content::NoXPath->import;
586             };
587              
588             BEGIN {
589             # Load the XML-variant if our prerequisites are there :
590 18         59 eval { install_libxml }
591 18 50 33 18   95 or eval { install_xpath }
  18         105  
592             or install_pureperl;
593             };
594              
595             {
596             package Test::HTML::Content::EmptyXPathResult;
597 0     0     sub size { 0 };
598 0     0     sub get_nodelist { () };
599             };
600              
601             1;
602              
603             __END__