File Coverage

blib/lib/Test/WWW/Selenium/HTML.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Test::WWW::Selenium::HTML;
2              
3 9     9   220636 use warnings;
  9         23  
  9         297  
4 9     9   57 use strict;
  9         19  
  9         354  
5              
6 9     9   9115 use Params::Validate qw(validate validate_pos SCALAR HASHREF);
  9         123496  
  9         979  
7 9     9   87 use Test::More;
  9         19  
  9         94  
8 9     9   3182 use Test::Builder;
  9         18  
  9         239  
9 9     9   10040 use Time::HiRes qw(sleep);
  9         20119  
  9         51  
10 9     9   18095 use XML::LibXML;
  0            
  0            
11              
12             # Accessors that do not take a locator as an argument.
13             use constant NO_LOCATOR =>
14             { map { $_ => 1 }
15             qw(Alert
16             AlertPresent
17             AllButtons
18             AllFields
19             AllLinks
20             BodyText
21             HtmlSource
22             Confirmation
23             Location
24             Prompt
25             Title) };
26              
27             # Accessors that store single values (strings, numbers).
28             use constant VALUE_STORES =>
29             { map { $_ => 1 }
30             qw(Attribute
31             Alert
32             BodyText
33             Confirmation
34             Cookie
35             CursorPosition
36             ElementWidth
37             ElementHeight
38             ElementIndex
39             ElementPositionLeft
40             ElementPositionTop
41             Eval
42             Expression
43             HtmlSource
44             Location
45             Prompt
46             SelectedId
47             SelectedIndex
48             SelectedLabel
49             SelectedValue
50             Table
51             Text
52             Title
53             Value
54             WhetherThisFrameMatchFrameExpression) };
55              
56             # Accessors that store an array of values.
57             use constant ARRAY_VALUE_STORES =>
58             { map { $_ => 1 }
59             qw(AllButtons
60             AllFields
61             AllLinks
62             AllWindowIds
63             AllWindowNames
64             AllWindowTitles
65             AttributeFromAllWindows
66             LogMessages
67             SelectedIds
68             SelectedIndexes
69             SelectedLabels
70             SelectedValues
71             SelectOptions) };
72              
73             # Accessors that store a boolean value.
74             use constant BOOL_VALUE_STORES =>
75             { map { $_ => 1 }
76             qw(AlertPresent
77             AlertNotPresent
78             Checked
79             NotChecked
80             ConfirmationPresent
81             ConfirmationNotPresent
82             Editable
83             NotEditable
84             ElementPresent
85             ElementNotPresent
86             Ordered
87             NotOrdered
88             PromptPresent
89             PromptNotPresent
90             SomethingSelected
91             NotSomethingSelected
92             TextPresent
93             TextNotPresent
94             Visible
95             NotVisible) };
96              
97             # All accessors.
98             use constant STORES =>
99             { map { $_ => 1 }
100             keys %{VALUE_STORES()},
101             keys %{ARRAY_VALUE_STORES()},
102             keys %{BOOL_VALUE_STORES()} };
103              
104             our $VERSION = '0.02';
105              
106             sub new
107             {
108             my $class = shift;
109             my ($selenium) = validate_pos(@_, { isa => 'Test::WWW::Selenium' });
110              
111             my $test = Test::Builder->new;
112             $test->exported_to(__PACKAGE__);
113              
114             my $self = { selenium => $selenium,
115             test_builder => $test,
116             vars => {},
117             diag_body_text_on_failure => 1 };
118            
119             bless $self, $class;
120              
121             return $self;
122             }
123              
124             # Takes an accessor name as its only argument. Returns a boolean
125             # indicating whether the accessor takes a locator argument.
126              
127             sub _has_no_locator
128             {
129             my ($accessor) = @_;
130              
131             return exists NO_LOCATOR->{$accessor};
132             }
133              
134             # Takes an L and returns the 'literal' content. At
135             # the moment, that involves joining all the text parts together and
136             # converting C
elements into newlines.
137              
138             sub _node_literal
139             {
140             my $node = shift;
141              
142             my $literal = '';
143             foreach my $child ($node->childNodes()) {
144             my $type = $child->nodeType;
145              
146             # Does the node require special processing?
147             if ( $child->nodeType == XML_ELEMENT_NODE
148             and $child->nodeName =~ /^br$/i) {
149             # BR elements need to be converted to newlines.
150             $literal .= "\n";
151             }
152             else {
153             # If it's not a 'special' node, just grab the text.
154             $literal .= $child->textContent();
155             }
156             }
157              
158             return $literal;
159             }
160              
161             # Takes the document element of an L as its
162             # single argument. Extracts and returns the Selenium test specifications
163             # from that XML as a list of arrayrefs. Each arrayref comprises the test
164             # command (e.g. C), the two arguments for the command
165             # and the line number where the test specification begins in the XML. If
166             # the command takes fewer than two arguments, the unused parts of the
167             # arrayref will contain empty strings.
168              
169             sub _xml_to_testdata
170             {
171             my ($doc) = @_;
172              
173             my $xmlns = $doc->getAttribute('xmlns');
174             if (not $xmlns) {
175             die "Test document must have an xmlns attribute.";
176             }
177             my $xpc = XML::LibXML::XPathContext->new($doc);
178             $xpc->registerNs('x', $xmlns);
179             my $trs = $xpc->findnodes('//x:tbody//x:tr');
180             my $trsc = $trs->size();
181             if (not $trsc) {
182             die "Test document contains no tests.";
183             }
184            
185             my @data =
186             map { [ (map { _node_literal($_) }
187             ($_->nonBlankChildNodes())),
188             $_->line_number() ] }
189             @{$trs};
190              
191             return @data;
192             }
193              
194             # Takes a string as its only argument. Converts it from 'camel-case' to
195             # 'perl-case' and returns it. For example, if passed the string
196             # C, this will return C.
197              
198             sub _perl_case
199             {
200             my ($str) = @_;
201            
202             $str =~ s/([a-z])([A-Z])/$1_\L$2/g;
203             $str = lcfirst $str;
204              
205             return $str;
206             }
207              
208             # Takes the variable hashref and a variable name as its arguments. If
209             # the name designates an array variable, returns the contents of that
210             # array as a single string, joined with commas. Otherwise, returns the
211             # scalar value of the variable.
212              
213             sub _var_to_string
214             {
215             my ($vars, $name) = @_;
216              
217             return
218             (ref $vars->{$name} eq 'ARRAY')
219             ? join ',', @{$vars->{$name}}
220             : $vars->{$name};
221             }
222              
223             # Takes the variable hashref and a string as its arguments. If the
224             # string contains variable placeholders, these are replaced with the
225             # corresponding values from the variable hashref. Returns the string
226             # after substitution has occurred.
227              
228             sub _substitute_vars
229             {
230             my ($vars, $str) = @_;
231              
232             my @varnames =
233             grep { exists $vars->{$_} }
234             ($str =~ /\${(.*?)}/g);
235            
236             for my $varname (@varnames) {
237             my $value = _var_to_string($vars, $varname);
238             $str =~ s/\${$varname}/$value/g;
239             }
240              
241             if ($str =~ /^javascript{.*}/) {
242             my @varnames =
243             grep { exists $vars->{$_} }
244             ($str =~ /storedVars\[['"](.*?)['"]\]/);
245            
246             for my $varname (@varnames) {
247             my $value = '"'.(_var_to_string($vars, $varname)).'"';
248             $str =~ s/storedVars\[['"]$varname['"]\]/$value/g;
249             }
250             }
251              
252             return $str;
253             }
254              
255             # Takes the L object as its only argument. Returns
256             # the timeout that should be used for operations that involve waiting.
257              
258             sub _get_timeout_in_seconds
259             {
260             my ($sel) = @_;
261              
262             # There's no other way to get the timeout from the
263             # Test::WWW::Selenium object, unfortunately.
264              
265             return
266             (defined $sel->{_timeout})
267             ? int ($sel->{_timeout} / 1000)
268             : 30;
269             }
270              
271             # Takes a pattern as its only argument. Returns a Regexp object for
272             # testing values against this pattern.
273              
274             sub _get_regexp_from_pattern
275             {
276             my ($pattern) = @_;
277              
278             if ($pattern =~ s/^regexp://) {
279             return qr/$pattern/;
280             }
281             if ($pattern =~ s/^exact://) {
282             return qr/\Q$pattern\E/;
283             }
284             for ($pattern) {
285             s/^glob://;
286             s/\*/\.\*/g;
287             s/\?/\./g;
288             }
289             return qr/$pattern/;
290             }
291              
292             # Takes a pattern and a boolean describing the 'sense' of the test
293             # command as its arguments (the boolean argument should be true for a
294             # test like C, and false for a test like
295             # C). Returns a verb that can be used in a test
296             # case string to describe the type of match that is occurring.
297              
298             sub _get_match_str_from_pattern
299             {
300             my ($pattern, $success) = @_;
301              
302             return
303             ($pattern =~ /^exact:/)
304             ? ($success)
305             ? 'equals'
306             : 'does not equal'
307             : ($success)
308             ? 'matches'
309             : 'does not match';
310             }
311              
312             # Takes a command name (any string), a target, a value, a boolean
313             # describing whether the command has no locator argument, a boolean
314             # describing whether the test command is a boolean accessor and a
315             # boolean describing the 'sense' of the test command (see
316             # L<_get_match_str_from_pattern>). Returns a string that can be used as
317             # the description of the test case.
318              
319             sub _get_test_case_str
320             {
321             my ($pcommand, $target, $value, $has_no_locator, $is_bool, $success) = @_;
322              
323             my $str = "$pcommand " .(($has_no_locator) ? '' : "'$target'");
324              
325             if ($is_bool) {
326             return $str;
327             }
328            
329             $value = ($has_no_locator ? $target : $value);
330             my $match_str = _get_match_str_from_pattern($value, $success);
331            
332             return "$str $match_str '$value'";
333             }
334            
335             # Takes an accessor type (the prefix of the accessor, e.g. C), an
336             # accessor name (including the type), a target (first test argument), a
337             # value (second test argument), the filename of the Selenium HTML test
338             # file, the line number of the test, a L object, a
339             # L object and a variable hashref as its arguments. Runs
340             # the accessor test using the L and returns a boolean
341             # describing whether the test succeeded. Updates the variable hashref if
342             # necessary, as well.
343              
344             sub _handle_accessor
345             {
346             my ($type, $command, $target, $value, $slnm_file, $line_number,
347             $sel, $tb, $vars, $diag) = @_;
348              
349             my ($part) = ($command =~ /^$type(.*)/);
350             my ($part_minus_not) = ($part =~ /Not([A-Z].*)/);
351            
352             if (not exists STORES->{$part}
353             and (not defined $part_minus_not
354             or not exists STORES->{$part_minus_not})) {
355             die "Invalid accessor '$part' at line $line_number.";
356             }
357              
358             my $is_bool = exists BOOL_VALUE_STORES->{$part};
359             my $is_array = exists ARRAY_VALUE_STORES->{$part};
360              
361             my $not = $part =~ s/Not([A-Z])/$1/;
362             my $has_no_locator = _has_no_locator($part);
363             my $pcmd = _perl_case($part);
364             my $get_pcmd = 'get_'.$pcmd;
365             my $is_pcmd = 'is_'.$pcmd;
366             my $pcommand = _perl_case($command);
367              
368             $value = ($has_no_locator ? $target : $value);
369             my @get_args = ($has_no_locator ? () : $target);
370             my $regexp = _get_regexp_from_pattern($value);
371              
372             my (undef, $test_file, $test_line_number) = $tb->caller(1);
373              
374             my $test_case_str =
375             _get_test_case_str($pcommand, $target, $value,
376             $has_no_locator, $is_bool, not $not).
377             " ($slnm_file:$line_number; ".
378             "$test_file:$test_line_number)";
379              
380             if ($type eq 'waitFor') {
381             my $timeout = _get_timeout_in_seconds($sel);
382              
383             my $test_coderef =
384             ($is_bool)
385             ? sub { my $res = $sel->$is_pcmd($target);
386             ($not) ? not $res : $res }
387             : sub { my $new_value = $sel->$get_pcmd(@get_args);
388             my $res = ($new_value =~ $regexp);
389             ($not) ? not $res : $res };
390              
391             WAIT: {
392             for (1..$timeout + 1) {
393             if (eval { $test_coderef->() }) {
394             pass($test_case_str);
395             last WAIT;
396             }
397             sleep(1);
398             }
399             $tb->ok(0, "$test_case_str ".
400             "(timed out, see line $line_number)");
401             if ($diag) {
402             my $html = "Response HTML source:\n".$sel->get_html_source();
403             $tb->diag($html);
404             }
405             return 0;
406             }
407             return 1;
408             }
409            
410             my @check_values =
411             ($is_bool)
412             ? $sel->$is_pcmd($target)
413             : $sel->$get_pcmd(@get_args);
414              
415             my $check_value = join ',', @check_values;
416              
417             if ($type eq 'store') {
418             $vars->{$value} =
419             ($is_array)
420             ? \@check_values
421             : $check_value;
422             return 1;
423             }
424              
425             my $res;
426             if ($is_bool) {
427             $res =
428             ($not)
429             ? $tb->ok((not $check_value), $test_case_str)
430             : $tb->ok(($check_value), $test_case_str);
431             } else {
432             $res =
433             ($not)
434             ? $tb->unlike($check_value, $regexp, $test_case_str)
435             : $tb->like( $check_value, $regexp, $test_case_str);
436             }
437            
438             if (not $res) {
439             if ($diag) {
440             my $html = "Response HTML source:\n".$sel->get_html_source();
441             $tb->diag($html);
442             }
443             if ($type eq 'assert') {
444             return 0;
445             }
446             }
447             return 1;
448             }
449              
450             # Takes a L object, a L object, a
451             # hashref of variables, the filename of the Selenium HTML test file and
452             # a test specification (as per L<_xml_to_testdata>) as its arguments.
453             # Runs the test and returns a boolean indicating whether the test was
454             # successful.
455            
456             sub _run_test
457             {
458             my ($sel, $tb, $vars, $slnm_file, $test, $diag) = @_;
459              
460             my ($command, $target, $value, $line_number) = @{$test};
461              
462             $target = _substitute_vars($vars, $target);
463             $value = _substitute_vars($vars, $value);
464              
465             my @accessor_args = ($command, $target, $value,
466             $slnm_file, $line_number,
467             $sel, $tb, $vars, $diag);
468              
469             my (undef, $test_file, $test_line_number) = $tb->caller();
470              
471             if ($command eq 'store') {
472             my $script;
473             $vars->{$value} =
474             (($script) = ($target =~ /^javascript{(.*)}$/))
475             ? $sel->get_eval($script)
476             : $target;
477             return 1;
478             }
479             if ($command eq 'echo') {
480             print $target."\n";
481             return 1;
482             }
483             if ($command eq 'waitForCondition') {
484             my ($script, $timeout) = ($target, $value);
485             if (not $timeout) {
486             $timeout = _get_timeout_in_seconds($sel) * 1000;
487             }
488             my $test_str = "waitForCondition($target, $value) ".
489             "($slnm_file:$line_number; ".
490             "$test_file:$test_line_number)";
491             my $res = eval { $sel->wait_for_condition($script, $timeout) };
492             my $error = $@;
493             $tb->ok($res, $test_str);
494             if ($error) {
495             $tb->diag($error);
496             }
497             return $res;
498             }
499             if ($command eq 'waitForPageToLoad') {
500             my $timeout = $target;
501             if (not $timeout) {
502             $timeout = _get_timeout_in_seconds($sel) * 1000;
503             }
504             my $test_str = "waitForPageToLoad($target, $value) ".
505             "($slnm_file:$line_number; ".
506             "$test_file:$test_line_number)";
507             my $res = eval { $sel->wait_for_page_to_load($timeout) };
508             my $error = $@;
509             $tb->ok($res, $test_str);
510             if ($error) {
511             $tb->diag($error);
512             }
513             return $res;
514             }
515             if (my ($type) = ($command =~ /(^assert|^verify|^waitFor|^store)/)) {
516             return _handle_accessor($type, @accessor_args);
517             }
518            
519             # Convert the command to 'perl-case' and append '_ok' (this will
520             # be the corresponding method name, assuming that it exists). If
521             # the command ends in 'AndWait', call C
522             # afterwards. Note that C cannot be used, because
523             # L uses autoload.
524            
525             my $pcmd = _perl_case($command);
526             my $wait = ($pcmd =~ s/_and_wait$//);
527             $pcmd .= '_ok';
528              
529             my $res = eval {
530             my (@args) = grep { defined $_ } ($target, $value);
531             # Including the command in the test description means that
532             # information is repeated for successful cases, but is
533             # necessary because the command is not included in the failure
534             # message (if the test fails).
535             my $test_desc = "$command(".(join ', ', @args).") ".
536             "($slnm_file:$line_number; ".
537             "$test_file:$test_line_number)";
538             my $res = $sel->$pcmd(@args, $test_desc);
539             if ($res and $wait) {
540             my $timeout = _get_timeout_in_seconds($sel) * 1000;
541             $sel->wait_for_page_to_load($timeout);
542             }
543             $res;
544             };
545             if (my $error = $@) {
546             my $str =
547             join ', ',
548             map { "'$_'" }
549             grep { $_ }
550             (@{$test})[0..2];
551             my $die_msg =
552             ($error =~ /Undefined subroutine|Can't locate.*method/)
553             ? "Unhandled command at $slnm_file:$line_number: [$str]"
554             : "Died while running test: $error: [$str]";
555             die $die_msg;
556             }
557             if (not $res) {
558             if ($diag) {
559             my $html = "Response HTML source:\n".$sel->get_html_source();
560             $tb->diag($html);
561             }
562             return 0;
563             }
564              
565             return 1;
566             }
567              
568             sub run
569             {
570             my $self = shift;
571              
572             my %args = validate(@_,
573             { data => { type => SCALAR, optional => 1 },
574             path => { type => SCALAR, optional => 1 }, }
575             );
576              
577             if (not $args{'data'} and not $args{'path'}) {
578             die "Either 'data' or 'path' must be provided.";
579             }
580             if ($args{'data'} and $args{'path'}) {
581             die "One (and only one) of 'data' and 'path' must be provided.";
582             }
583              
584             my $parser = XML::LibXML->new();
585             $parser->load_ext_dtd(0);
586             $parser->line_numbers(1);
587            
588             if ($args{'path'}) {
589             open my $fh, '<', $args{'path'}
590             or die "Unable to open ".$args{'path'}.": $!";
591             $args{'data'} = do { local $/; <$fh> };
592             close $fh;
593             }
594              
595             my $filename = $args{'path'} ? $args{'path'} : '[no filename]';
596            
597             my $doc = $parser->parse_string($args{'data'});
598             my @testdata = _xml_to_testdata($doc->getDocumentElement());
599            
600             my $sel = $self->{'selenium'};
601             if (not $self->{'opened'}) {
602             $self->{'opened'} = 1;
603             $sel->open('/');
604             }
605              
606             for my $test (@testdata) {
607             my $res = _run_test($sel, $self->{'test_builder'},
608             $self->{'vars'}, $filename, $test,
609             $self->diag_body_text_on_failure());
610             if (not $res) {
611             return;
612             }
613             }
614              
615             return 1;
616             }
617              
618             sub vars
619             {
620             my ($self) = @_;
621              
622             return $self->{'vars'};
623             }
624              
625             sub diag_body_text_on_failure
626             {
627             my ($self, $value) = @_;
628              
629             my $current_value = $self->{'diag_body_text_on_failure'};
630              
631             if (@_ == 2) {
632             $self->{'diag_body_text_on_failure'} = $value;
633             }
634              
635             return $current_value;
636             }
637              
638             1;
639              
640             __END__