File Coverage

blib/lib/Test/HTML/Form.pm
Criterion Covered Total %
statement 210 225 93.3
branch 55 84 65.4
condition 13 28 46.4
subroutine 33 33 100.0
pod 16 16 100.0
total 327 386 84.7


line stmt bran cond sub pod time code
1             package Test::HTML::Form;
2 3     3   125457 use strict;
  3         12  
  3         69  
3 3     3   10 use warnings;
  3         5  
  3         67  
4 3     3   10 no warnings 'redefine';
  3         6  
  3         107  
5              
6             =head1 NAME
7              
8             Test::HTML::Form - HTML Testing and Value Extracting
9              
10             =head1 VERSION
11              
12             1.00
13              
14             =head1 SYNOPSIS
15              
16             use Test::HTML::Form;
17              
18             my $filename = 't/form_with_errors.html';
19              
20             my $response = $ua->request($request)
21              
22             # test functions
23              
24             title_matches($filename,'Foo Bar','title matches');
25              
26             no_title($filename,'test site','no english title');
27              
28             tag_matches($response,
29             'p',
30             { class => 'formError',
31             _content => 'There is an error in this form.' },
32             'main error message appears as expected' );
33              
34             no_tag($filename,
35             'p',
36             { class => 'formError',
37             _content => 'Error' },
38             'no unexpected errors' );
39              
40              
41             text_matches($filename,'koncerty','found text : koncerty'); # check text found in file
42              
43             no_text($filename,'Concert','no text matching : Concert'); # check text found in file
44              
45             image_matches($filename,'/images/error.gif','matching image found image in HTML');
46              
47             link_matches($filename,'/post/foo.html','Found link in HTML');
48              
49             script_matches($response, qr/function someWidget/, 'found widget in JS');
50              
51             form_field_value_matches($response,'category_id', 12345678, undef, 'category_id matches');
52              
53             form_select_field_matches($filename,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
54              
55             form_checkbox_field_matches($response,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
56              
57             # Data extraction functions
58              
59             my $form_values = Test::HTML::Form->get_form_values({filename => $filename, form_name => 'form1'});
60              
61             my $posting_id = Test::HTML::Form->extract_text({filename => 'publish.html', pattern => 'Reference :\s(\d+)'});
62              
63             =head1 DESCRIPTION
64              
65             Test HTML pages and forms, and extract values.
66              
67             Developed for and released with permission of Slando (http://www.slando.com)
68              
69             All test functions will take either a filename or an HTTP::Response compatible object (i.e. any object with a content method)
70              
71             =cut
72              
73 3     3   469 use Data::Dumper;
  3         5114  
  3         133  
74 3     3   1752 use HTML::TreeBuilder;
  3         75033  
  3         27  
75              
76 3     3   106 use base qw( Exporter Test::Builder::Module);
  3         5  
  3         4784  
77             our @EXPORT = qw(
78             link_matches no_link
79             image_matches no_image
80             tag_matches no_tag
81             text_matches no_text
82             script_matches
83             title_matches no_title
84             form_field_value_matches
85             form_select_field_matches
86             form_checkbox_field_matches
87             );
88              
89             my $Test = Test::Builder->new;
90             my $CLASS = __PACKAGE__;
91             my %parsed_files = ();
92             my %parsed_file_forms = ();
93              
94             our $VERSION = 1.00;
95              
96             =head1 FUNCTIONS
97              
98             =head2 image_matches
99              
100             Test that some HTML contains an img tag with a src attribute matching the link provided.
101              
102             image_matches($filename,$image_source,'matching image found image in HTML');
103              
104             Passes when at least one instance found, fails if no matches found.
105              
106             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
107              
108             =cut
109              
110             sub image_matches {
111 1     1 1 6 my ($filename,$link,$name) = (@_);
112 1         3 local $Test::Builder::Level = 2;
113 1         7 return tag_matches($filename,'img',{ src => $link },$name);
114             };
115              
116              
117             =head2 no_image
118              
119             Test that some HTML doesn't contain any img tag with a src attribute matching the link provided.
120              
121             no_image($response,$image_source,'no matching image found in HTML');
122              
123             Passes when no matches found, fails if any matches found.
124              
125             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
126              
127             =cut
128              
129             sub no_image {
130 1     1 1 6 my ($filename,$link,$name) = (@_);
131 1         2 local $Test::Builder::Level = 2;
132 1         4 return no_tag($filename,'img',{ src => $link },$name);
133             };
134              
135              
136             =head2 link_matches
137              
138             Test that some HTML contains a href tag with a src attribute matching the link provided.
139              
140             link_matches($response,$link_destination,'Found link in HTML');
141              
142             Passes when at least one instance found, fails if no matches found.
143              
144             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
145              
146             =cut
147              
148             sub link_matches {
149 3     3 1 1377 my ($filename,$link,$name) = (@_);
150 3         4 local $Test::Builder::Level = 2;
151 3         13 return tag_matches($filename,['a','link'],{ href => $link },$name);
152             };
153              
154             =head2 no_link
155              
156             Test that some HTML does not contain a href tag with a src attribute matching the link provided.
157              
158             link_matches($filename,$link_destination,'Link not in HTML');
159              
160             Passes when if no matches found, fails when at least one instance found.
161              
162             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
163              
164             =cut
165              
166             sub no_link {
167 1     1 1 6 my ($filename,$link,$name) = (@_);
168 1         2 local $Test::Builder::Level = 2;
169 1         4 return no_tag($filename,'a',{ href => $link },$name);
170             };
171              
172             =head2 title_matches
173              
174             Test that some HTML contains a title tag with content matching the pattern/string provided.
175              
176             title_matches($filename,'Foo bar home page','title matches');
177              
178             Passes when at least one instance found, fails if no matches found.
179              
180             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
181              
182             =cut
183              
184             sub title_matches {
185 1     1 1 68 my ($filename,$title,$name) = @_;
186 1         2 local $Test::Builder::Level = 2;
187 1         4 return tag_matches($filename,"title", { _content => $title } ,$name);
188             };
189              
190             =head2 no_title
191              
192             Test that some HTML does not contain a title tag with content matching the pattern/string provided.
193              
194             no_title($filename,'Foo bar home page','title matches');
195              
196             Passes if no matches found, fails when at least one instance found.
197              
198             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
199              
200             =cut
201              
202             sub no_title {
203 1     1 1 8 my ($filename,$title,$name) = (@_);
204 1         2 local $Test::Builder::Level = 2;
205 1     1   7 return no_tag($filename,'title', sub { shift->as_trimmed_text =~ m/$title/ },$name);
  1         59  
206             }
207              
208              
209             =head2 tag_matches
210              
211             Test that some HTML contains a tag with content or attributes matching the pattern/string provided.
212              
213             tag_matches($filename,'a',{ href => $link },$name); # check matching tag found in file
214              
215             Passes when at least one instance found, fails if no matches found.
216              
217             Takes a list of arguments
218              
219             =over 4
220              
221             =item filename/response - string of path/name of file, or an HTTP::Response object
222              
223             =item tag type(s) - string or arrarref of strings naming which tag(s) to match
224              
225             =item attributes - hashref of attributes and strings or quoted-regexps to match
226              
227             =item comment - an optional test comment/name
228              
229             =back
230              
231             =cut
232              
233             sub tag_matches {
234 7     7 1 50 my ($filename, $tag, $attr_ref, $name) = @_;
235 7         11 my $count = 0;
236              
237 7 100       17 if (ref $tag ) {
238 4         8 foreach my $this_tag (@$tag) {
239 8         16 $count += _tag_count($filename, $this_tag, $attr_ref);
240             }
241             } else {
242 3         5 $count = _tag_count($filename, $tag, $attr_ref);
243             }
244              
245 7         58 my $tb = $CLASS->builder;
246 7         71 my $ok = $tb->ok( $count, $name);
247 7 100       2474 unless ($ok) {
248 1 50       5 my $tagname = ( ref $tag ) ? join (' or ', @$tag) : $tag ;
249 1         5 $tb->diag("Expected at least one tag of type '$tagname' in file $filename matching condition, but got 0\n");
250             }
251 7         201 return $ok;
252             }
253              
254              
255              
256             =head2 no_tag
257              
258             Test that some HTML does not contain a tag with content or attributes matching the pattern/string provided.
259              
260             no_tag($filename,'a',{ href => $link },$name); # check matching tag NOT found in file
261              
262             Passes if no matches found, fails when at least one instance found.
263              
264             Takes a list of arguments filename/response, hashref of attributes and strings or quoted-regexps to match, and optional test comment/name
265              
266             =cut
267              
268             sub no_tag {
269 4     4 1 13 my ($filename,$tag,$attr_ref,$name) = @_;
270 4         7 my $count = _tag_count($filename, $tag, $attr_ref);
271 4         12 my $tb = $CLASS->builder;
272 4         32 my $ok = $tb->is_eq( $count, 0, $name);
273 4 50       1863 unless ($ok) {
274 0         0 $tb->diag("Expected no tags of type $tag matching criteria in file $filename, but got $count\n");
275             }
276 4         9 return $ok;
277             };
278              
279             =head2 text_matches
280              
281             Test that some HTML contains some content matching the pattern/string provided.
282              
283             text_matches($filename,$text,$name); # check text found in file
284              
285             Passes when at least one instance found, fails if no matches found.
286              
287             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
288              
289             =cut
290              
291             sub text_matches {
292 1     1 1 5 my ($filename,$text,$name) = @_;
293 1         5 my $count = _count_text({filename => $filename, text => $text });
294 1         4 my $tb = $CLASS->builder;
295 1         9 my $ok = $tb->ok( $count, $name);
296 1 50       226 unless ($ok) {
297 0         0 $tb->diag("Expected HTML to contain at least one instance of text '$text' in file $filename but not found\n");
298             }
299 1         3 return $ok;
300             };
301              
302             =head2 no_text
303              
304             Test that some HTML does not contain some content matching the pattern/string provided.
305              
306             no_text($filename,$text,$name); # check text NOT found in file
307              
308             Passes if no matches found, fails when at least one instance found.
309              
310             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
311              
312             =cut
313              
314             sub no_text {
315 1     1 1 4 my ($filename,$text,$name) = @_;
316 1         4 my $count = _count_text({filename => $filename, text => $text });
317 1         6 my $tb = $CLASS->builder;
318 1         10 my $ok = $tb->is_eq( $count, 0 , $name);
319 1 50       515 unless ($ok) {
320 0         0 $tb->diag("Expected HTML to not contain text '$text' in file $filename but $count instances found\n");
321             }
322 1         3 return $ok;
323             };
324              
325              
326             =head2 script_matches
327              
328             Test that HTML script element contains text matcging that provided.
329              
330             script_matches($response, qr/function someWidget/, 'found widget in JS');
331              
332             Passes when at least one instance found, fails if no matches found.
333              
334             Takes a list of arguments filename/response, string or quoted-regexp to match, and optional test comment/name
335              
336             =cut
337              
338             sub script_matches {
339 1     1 1 7 my ($filename,$text_to_match,$name) = @_;
340 1         2 my $pattern;
341 1 50       4 if (ref($text_to_match) eq 'Regexp') {
342 1         1 $pattern = $text_to_match;
343             }
344 1         3 my $tree = _get_tree($filename);
345              
346             my @parse_args = sub {
347 1     1   82 my $elem = shift;
348 1 50       3 return 0 unless (ref $elem eq 'HTML::Element' );
349 1         3 my $ok = 0;
350 1         3 (my $text = $elem->as_HTML) =~ s/<(.|\n)*?>//g;
351 1 50       335 if ($pattern) {
352 1         6 my $ok = $text =~ m/$pattern/;
353 1   33     4 return $ok || $text =~ m/$pattern/;
354             } else {
355 0         0 $text eq $text_to_match;
356             }
357 1         5 };
358              
359 1         5 my $count = $tree->look_down( _tag => 'script', @parse_args );
360              
361 1         8 my $tb = $CLASS->builder;
362 1         17 my $ok = $tb->ok( $count, $name);
363 1 50       230 unless ($ok) {
364 0         0 $tb->diag("Expected script tag in file $filename matching $text_to_match, but got 0\n");
365             }
366 1         6 return $ok;
367             };
368              
369              
370              
371             =head2 form_field_value_matches
372              
373             Test that the HTML contains a form element with the value matching that provided.
374              
375             form_field_value_matches($filename,$field_name, $field_value, $form_name, $description);
376              
377             form_field_value_matches($filename,$field_name, qr/some pattern/, undef, 'test for foo in bar form field');
378              
379             Takes a list of arguments : filename/response, string or quoted-regexp to match, optional form_name, and optional test comment/name
380              
381             Field value argument can be a string (for exact matches) or a quoted regexp (for pattern matches)
382              
383             Use form_select_field_matches for select elements.
384              
385             Use form_checkbox_field_matches for checkbox elements
386              
387             =cut
388              
389             sub form_field_value_matches {
390 2     2 1 19 my ($filename,$field_name, $field_value, $form_name, $description) = @_;
391 2         8 my $form_fields = __PACKAGE__->get_form_values({ filename => $filename, form_name => $form_name });
392 2         7 my $tb = $CLASS->builder;
393              
394 2         15 my $elems = $form_fields->{$field_name};
395              
396 2         3 my $ok = 0;
397 2         3 foreach my $elem (@$elems) {
398 2         12 my $matches = _compare($elem,$field_value);
399 2 50       12 if ($matches) {
400 2         25 $ok = $tb->ok( $matches , $description);
401 2         503 last;
402             }
403             }
404              
405 2 50       5 unless ($ok) {
406 0         0 $tb->ok( 0 , $description);
407 0         0 $tb->diag("Expected form to contain field '$field_name' and have value of '$field_value' but not found in file $filename\n");
408             }
409 2         3 return $ok;
410             };
411              
412             =head2 form_select_field_matches
413              
414             Test that the HTML contains a form element with the value matching that provided.
415              
416             form_select_field_matches($filename,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
417              
418             Takes a mixed list/ hashref of arguments :
419              
420             =over 4
421              
422             =item filename/response,
423              
424             =item hashref of search attributes, keys are : field_name, selected, form_name (optional)
425              
426             =item optional test comment/name
427              
428             =back
429              
430             Selected field value can be string or quoted regexp
431              
432             =cut
433              
434             sub form_select_field_matches {
435 2     2 1 1938 my ($filename, $field_value_args, $description) = @_;
436 2         9 my $form_fields = __PACKAGE__->get_form_values({ filename => $filename, form_name => $field_value_args->{form_name} });
437 2         7 my $tb = $CLASS->builder;
438 2         15 my $ok = 0;
439 2         3 my $field_value = $field_value_args->{selected};
440 2         3 my $field_name = $field_value_args->{field_name};
441              
442 2         12 my $select_elem = $form_fields->{$field_name}[0];
443              
444              
445 2 100       6 if ($select_elem) {
446 1 50       5 unless (UNIVERSAL::can($select_elem,'descendants')) {
447 0         0 die "$select_elem (",$select_elem->tag,") is not a select html element for field : $field_name - did you mean to call form_checkbox_field_matches ?";
448             }
449 1         1 my $selected_option;
450 1         3 foreach my $option ( $select_elem->descendants ) {
451 10 50 33     527 next unless (ref($option) && ( lc($option->tag) eq 'option') );
452 10 100       61 if ( _compare($option, $field_value) ) {
453 1         1 $selected_option = $option;
454 1         2 last;
455             }
456             }
457              
458 1   50     6 $ok = $tb->ok( $selected_option && scalar grep (m/selected/i && $selected_option->attr($_), $selected_option->all_external_attr_names), $description);
459             } else {
460 1         2 $ok = $tb->ok(0, $description);
461             }
462 2 100       1036 unless ($ok) {
463 1         6 $tb->diag("Expected form to contain field '$field_name' and have option with value of '$field_value' selected but not found in file $filename \n");
464             }
465 2         188 return $ok;
466             }
467              
468             =head2 form_checkbox_field_matches
469              
470             Test that the HTML contains a form element with the value matching that provided.
471              
472             form_checkbox_field_matches($filename,{ field_name => $field_name, selected => $field_value, form_name => $form_name}, $description);
473              
474             Takes a mixed list/ hashref of arguments :
475              
476             =over 4
477              
478             =item filename/response,
479              
480             =item hashref of search attributes, keys are : field_name, selected, form_name (optional)
481              
482             =item optional test comment/name
483              
484             =back
485              
486             Selected field value can be string or quoted regexp
487              
488             =cut
489              
490             sub form_checkbox_field_matches {
491 1     1 1 6 my ($filename, $field_value_args, $description) = @_;
492 1         5 my $form_fields = __PACKAGE__->get_form_values({ filename => $filename, form_name => $field_value_args->{form_name} });
493 1         10 my $tb = $CLASS->builder;
494              
495 1         9 my $field_value = $field_value_args->{selected};
496 1         1 my $field_name = $field_value_args->{field_name};
497 1         2 my $selected_box;
498 1   50     3 my $checkbox_elems = $form_fields->{$field_name} || [];
499              
500 1         3 foreach my $checkbox ( @$checkbox_elems ) {
501 1 50       2 if ( _compare($checkbox, $field_value) ) {
502 1         2 $selected_box = $checkbox;
503 1         2 last;
504             }
505             }
506              
507 1   50     5 my $ok = $tb->ok( $selected_box && scalar grep (m/checked/i && $selected_box->attr($_), $selected_box->all_attr_names), $description);
508 1 50       266 unless ($ok) {
509 0         0 $tb->diag("Expected form to contain field '$field_name' and have option with value of '$field_value' selected but not found in file $filename\n");
510             }
511 1         3 return $ok;
512             }
513              
514             =head2 get_form_values
515              
516             Extract form fields and their values from HTML content
517              
518             my $form_values = Test::HTML::Form->get_form_values({filename => $filename, form_name => 'form1'});
519              
520             Takes a hashref of arguments : filename (name of file or an HTTP::Response object, required), form_name (optional).
521              
522             Returns a hashref of form fields, with name as key, and arrayref of XML elements for that field.
523              
524             =cut
525              
526             sub get_form_values {
527 6     6 1 489 my $class = shift;
528 6         8 my $args = shift;
529 3     3   20 no warnings 'uninitialized';
  3         11  
  3         2616  
530 6         9 my $form_name = $args->{form_name};
531 6         11 my $internal_form_name = $form_name . ' form';
532 6 100       18 if ($parsed_file_forms{$args->{filename}}{$internal_form_name}) {
533 4         15 return $parsed_file_forms{$args->{filename}}{$internal_form_name};
534             } else {
535 2         5 my $tree = _get_tree($args->{filename});
536 2         4 my $form_fields = { };
537             my ($form) = $tree->look_down('_tag', 'form',
538             sub {
539 2     2   237 my $form = shift;
540 2 50       5 if ($form_name) {
541 0 0       0 return 1 if $form->attr('name') eq $form_name;
542 0 0       0 return 1 if $form->attr('id') eq $form_name;
543             } else {
544 2         4 return 1;
545             }
546             }
547 2         12 );
548 2 50       902 if (ref $form) {
549 2         9 my @form_nodes = $form->descendants();
550 2         2602 foreach my $node (@form_nodes) {
551 126 50       727 next unless (ref($node));
552 126 100       140 if (lc($node->tag) =~ /^(input|select|textarea|button)$/i) {
553 22 100       134 if (lc $node->attr('type') =~ /(radio|checkbox)/) {
554 6         57 push (@{$form_fields->{$node->attr('name')}},$node);
  6         11  
555             } else {
556 16         150 $form_fields->{$node->attr('name')} = [ $node ];
557             }
558             }
559             }
560             }
561 2         22 $parsed_file_forms{$args->{filename}}{$internal_form_name} = $form_fields;
562              
563 2         5 return $form_fields;
564             }
565             }
566              
567             =head2 extract_text
568              
569             my $posting_id = Test::HTML::Form->extract_text({filename => 'publish.html', pattern => 'Reference :\s(\d+)'});
570              
571             =cut
572              
573             sub extract_text {
574 1     1 1 7 my $class = shift;
575 1         1 my $args = shift;
576 1         3 my $tree = _get_tree($args->{filename});
577 1         2 my $pattern = $args->{pattern};
578             my ($node) = $tree->look_down( sub {
579 76     76   4478 my $thisnode = shift;
580 76         116 $thisnode->normalize_content;
581 76 100       1316 return 1 if ($thisnode->as_trimmed_text =~ m/$pattern/i);
582 1         5 });
583 1         30 my ($match) = ($node->as_trimmed_text =~ m/$pattern/i);
584              
585 1         518 return $match;
586             }
587              
588              
589              
590             #
591             ##########################################
592             # Private / Internal methods and functions
593              
594             sub _compare {
595 13     13   20 my ($elem, $field_value) = @_;
596 13 50 33     33 unless ($elem && (ref$elem eq 'HTML::Element') ) {
597 0         0 warn "_compare passed $elem and value $field_value, $elem should be HTML::Element but is : ", ref $elem, "\n";
598 0         0 return 0 ;
599             }
600              
601 13 50       19 my $have_regexp = ( ref($field_value) eq 'Regexp' ) ? 1 : 0;
602 13         38 my $value = $elem->attr('value') ;
603 13 100       114 unless (defined $value) {
604 11         13 $value = $elem->as_trimmed_text;
605             }
606 13         314 my $ok;
607 13 50       16 if ($have_regexp) {
608 0 0 0     0 $ok = ( $elem && $value =~ m/$field_value/ ) ? 1 : 0 ;
609             } else {
610 13 100 66     33 $ok = ( $elem && $value eq $field_value ) ? 1 : 0 ;
611             }
612 13         28 return $ok
613             }
614              
615             sub _tag_count {
616 15     15   25 my ($filename, $tag, $attr_ref) = @_;
617 15         28 my $tree = _get_tree($filename);
618 15         20 my @parse_args = ();
619 15 100       29 if ( ref $attr_ref eq 'HASH' ) {
620 14         11 my $pattern;
621 14 100       29 if (ref($attr_ref->{_content}) eq 'Regexp') {
622 3         4 $pattern = $attr_ref->{_content};
623 3         5 delete $attr_ref->{_content};
624             }
625              
626 14         30 @parse_args = %$attr_ref ;
627 14 100       23 if ($pattern) {
628             push( @parse_args, sub {
629 2 50   2   215 return 0 unless (ref $_[0] eq 'HTML::Element' );
630 2         6 return $_[0]->as_trimmed_text =~ m/$pattern/;
631 3         11 } );
632             }
633             } else {
634 1         3 @parse_args = $attr_ref ;
635             }
636 15         47 my $count = $tree->look_down( _tag => $tag, @parse_args );
637              
638 15   100     5565 return $count || 0;
639             }
640              
641              
642             sub _count_text {
643 2     2   4 my $args = shift;
644 2         4 my $tree = _get_tree($args->{filename});
645 2         2 my $text = $args->{text};
646             my $count = $tree->look_down( sub {
647 77     77   4732 my $node = shift;
648 77         134 $node->normalize_content;
649 77 100       1284 return 1 if ($node->as_trimmed_text =~ m/$text/);
650 2         11 });
651 2   100     564 return $count || 0;
652             }
653              
654             sub _get_tree {
655 21     21   28 my $filename = shift;
656 21 100       38 unless ($parsed_files{$filename}) {
657 2         15 my $tree = HTML::TreeBuilder->new;
658 2         536 $tree->store_comments(1);
659 2 50 33     26 if (ref $filename && $filename->can('content')) {
660 0         0 $tree->parse_content($filename->decoded_content);
661             } else {
662 2 50       40 die "can't find file $filename" unless (-f $filename);
663 2         21 $tree->parse_file($filename);
664             }
665 2         42352 $parsed_files{$filename} = $tree;
666             }
667 21         31 return $parsed_files{$filename};
668             }
669              
670              
671             =head1 SEE ALSO
672              
673             =over 4
674              
675             =item Test::HTML::Content
676              
677             =item HTML::TreeBuilder
678              
679             =item Test::HTTP::Response
680              
681             =back
682              
683             =head1 AUTHOR
684              
685             Aaron Trevena
686              
687             =head1 BUGS
688              
689             Please report any bugs or feature requests to http://rt.cpan.org
690              
691             =head1 COPYRIGHT & LICENSE
692              
693             Copyright 2008 Slando.
694             Copyright 2009 Aaron Trevena.
695              
696             This library is free software; you can redistribute it and/or modify
697             it under the same terms as Perl itself, either Perl version 5.8.8 or,
698             at your option, any later version of Perl 5 you may have available.
699              
700             =cut
701              
702              
703             1;