File Coverage

blib/lib/Selenium/Element.pm
Criterion Covered Total %
statement 15 185 8.1
branch 0 178 0.0
condition 0 55 0.0
subroutine 5 33 15.1
pod 27 27 100.0
total 47 478 9.8


line stmt bran cond sub pod time code
1             package Selenium::Element;
2              
3 2     2   8 use strict;
  2         3  
  2         59  
4 2     2   7 use warnings;
  2         3  
  2         47  
5              
6 2     2   6 use Carp;
  2         828  
  2         135  
7 2     2   10 use Scalar::Util qw(blessed reftype looks_like_number);
  2         4  
  2         111  
8 2     2   1375 use Data::Random;
  2         4546  
  2         4024  
9              
10             =head1 NAME
11              
12             Selenium::Element - Unified way of interacting with Selenium & WebDriver elements, with a focus on inputs.
13              
14             =head1 SYNOPSIS
15              
16             Smooths out the interface between WWW::Selenium and Selenium::Remote::Driver elements.
17             Also creates a unified set/get interface for all inputs.
18              
19             =head1 CONSTRUCTOR
20              
21             =head2 new(ELEMENT,DRIVER,SELECTOR)
22              
23             Create a new Selenium::Element. You should never have to use/override this except in the most extreme of circumstances.
24             Use getElement/getElements instead.
25              
26             B:
27              
28             I - Either the WWW::Selenium locator string or a Selenium::Remote::WebElement, depending on your driver
29              
30             I - Either a WWW::Selenium element or false, depending on your driver (the WebElement has the driver in the latter case)
31              
32             I - Arrayref of the form [selector,selectortype]
33              
34             B:
35              
36             new Selenium::Element
37              
38             =cut
39              
40             sub new {
41 0     0 1   my ($class,$element,$driver,$selector) = @_;
42 0 0         confess("Constructor must be called statically, not by an instance") if ref($class);
43 0 0         return undef if !$element;
44 0 0 0       confess("Element driver invalid: must be WWW::Selenium object or false (element is a Selenium::Remote::Webelement)") unless $driver == 0 || (blessed($driver) && blessed($driver) eq 'WWW::Selenium' );
      0        
45              
46 0           my $self = {
47             'driver' => $driver,
48             'element' => $element,
49             'selector' => $selector
50             };
51              
52 0           bless $self, $class;
53 0           return $self;
54             }
55              
56             =head1 GETTERS
57              
58             =head2 get_tag_name
59              
60             Returns the tag name of the Element object.
61              
62             =cut
63              
64             sub get_tag_name {
65 0     0 1   my ($self) = @_;
66 0 0         confess("Object parameters must be called by an instance") unless ref($self);
67 0 0         if ($self->{'driver'}) {
68 0           my @parts = split(qr/=/,$self-{'element'});
69             #TODO If you can't do it with both of these, you have no business doing it...but this could be expanded to everything eventually...
70 0 0         confess('WWW::Selenium drivers can only get tag name if selector is of type "id" or "css"') unless scalar(grep {$_ eq $parts[0]} qw(id css));
  0            
71 0 0         my $js = $parts[0] eq 'id' ? 'document.getElementById("'.$parts[1].'").nodeName' : 'document.querySelectorAll("'.$parts[1].'")[0].nodeName';
72 0           return lc($self->javascript($js));
73             }
74 0           return $self->{'element'}->get_tag_name();
75             }
76              
77             =head2 get_type
78              
79             Returns the type of the Element object if it is an input tag.
80              
81             =cut
82              
83             sub get_type {
84 0     0 1   my $self = shift;
85 0 0         confess("Object parameters must be called by an instance") unless ref($self);
86 0 0         return undef unless $self->is_input;
87 0 0         return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'}.'@type') : $self->{'element'}->get_attribute('type');
88             }
89              
90             #Input specific stuf
91             #TODO cache the results of all this stuff?
92              
93             =head2 is_input
94              
95             Returns whether the element is an input.
96              
97             =cut
98              
99             sub is_input {
100 0     0 1   my ($self) = @_;
101 0 0         confess("Object parameters must be called by an instance") unless ref($self);
102 0           return $self->get_tag_name() eq 'input';
103             }
104              
105             =head2 is_textinput
106              
107             Returns whether the element is an input with type 'text' or 'password' or a textarea.
108              
109             =cut
110              
111             sub is_textinput {
112 0     0 1   my ($self) = @_;
113 0 0         confess("Object parameters must be called by an instance") unless ref($self);
114 0           my $itype = $self->get_type();
115 0           my $ret = scalar(grep {$_ eq $itype} ('password', 'text'));
  0            
116 0   0       return $ret || $self->get_tag_name() eq 'textarea';
117             }
118              
119             =head2 is_select
120              
121             Returns whether the element is a select.
122              
123             =cut
124              
125             sub is_select {
126 0     0 1   my ($self) = @_;
127 0 0         confess("Object parameters must be called by an instance") unless ref($self);
128 0           return $self->get_tag_name() eq 'select';
129             }
130              
131             =head2 is_multiselect
132              
133             Returns whether the element is a select with the 'multiple' attribute.
134              
135             =cut
136              
137             sub is_multiselect {
138 0     0 1   my $self = shift;
139 0 0         confess("Object parameters must be called by an instance") unless ref($self);
140 0 0         return 0 if !$self->is_select;
141 0 0         return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'},'multiple') : $self->{'element'}->get_attribute('multiple');
142             }
143              
144             =head2 is_radio
145              
146             Returns whether the element is a radio button.
147              
148             =cut
149              
150             sub is_radio {
151 0     0 1   my ($self) = @_;
152 0 0         confess("Object parameters must be called by an instance") unless ref($self);
153 0           return $self->get_type() eq 'radio';
154             }
155              
156             =head2 is_checkbox
157              
158             Returns whether the element is a checkbox.
159              
160             =cut
161              
162             sub is_checkbox {
163 0     0 1   my ($self) = @_;
164 0 0         confess("Object parameters must be called by an instance") unless ref($self);
165 0           return $self->get_type() eq 'checkbox';
166             }
167              
168             =head2 is_submit
169              
170             Returns whether the element is an input of the type 'submit'.
171              
172             =cut
173              
174             sub is_submit {
175 0     0 1   my ($self) = @_;
176 0 0         confess("Object parameters must be called by an instance") unless ref($self);
177 0           return $self->get_type() eq 'submit';
178             }
179              
180             =head2 is_fileinput
181              
182             Returns whether the element is an input of the type 'file'.
183              
184             =cut
185              
186             sub is_fileinput {
187 0     0 1   my ($self) = @_;
188 0 0         confess("Object parameters must be called by an instance") unless ref($self);
189 0           return $self->get_type() eq 'file';
190             }
191              
192             =head2 is_form
193              
194             Returns whether the element is a form.
195              
196             =cut
197              
198             sub is_form {
199 0     0 1   my ($self) = @_;
200 0 0         confess("Object parameters must be called by an instance") unless ref($self);
201 0 0         confess("WWW::Selenium does not support getting tag type of elements") if $self->{'driver'};
202 0           return $self->get_tag_name() eq 'form';
203             }
204              
205             =head2 is_option
206              
207             Returns whether the element is an option.
208              
209             =cut
210              
211             sub is_option {
212 0     0 1   my ($self) = @_;
213 0 0         confess("Object parameters must be called by an instance") unless ref($self);
214 0 0         confess("WWW::Selenium does not support getting tag type of elements") if $self->{'driver'};
215 0           return $self->get_tag_name() eq 'option';
216             }
217              
218             =head2 is_hiddeninput
219              
220             Returns whether the element is an input of type 'hidden'.
221              
222             =cut
223              
224             sub is_hiddeninput {
225 0     0 1   my $self = shift;
226 0 0         confess("Object parameters must be called by an instance") unless ref($self);
227 0           return $self->get_type() eq 'hidden';
228             }
229              
230             =head2 is_enabled
231              
232             Returns whether the element is a disabled input.
233              
234             =cut
235              
236             sub is_enabled {
237 0     0 1   my ($self) = @_;
238 0 0         confess("Object parameters must be called by an instance") unless ref($self);
239             #Note that this will be more or less a no-op for WWW::Selenium, as there's no real way to get the tag name, so we will never see this branch
240 0 0         return $self->{'driver'} ? $self->{'driver'}->is_editable($self->{'element'}) : $self->{'element'}->is_enabled();
241             }
242              
243             =head2 get_options
244              
245             Returns a list containing Selenium::Element objects that are child options, if this object is a select.
246              
247             =cut
248              
249             sub get_options {
250 0     0 1   my $self = shift;
251 0 0         confess("Object parameters must be called by an instance") unless ref($self);
252 0 0         return () unless $self->is_select();
253 0           my @options = ();
254 0 0         if ($self->{'driver'}) {
255             #XXX obviously not reliable
256 0           carp("WARNING: WWW::Selenium has reduced ability to get options! This may not work as you expect.");
257 0           my @labels = $self->{'driver'}->get_select_options($self->{'element'});
258 0           return map {Selenium::Element->new("css=option[value=$_]",$self->{'driver'})} @labels;
  0            
259             }
260 0           my @opts = $self->{'element'}->{'driver'}->find_child_elements($self->{'element'},'option','tag_name');
261 0           return map {Selenium::Element->new($_,0)} @opts;
  0            
262             }
263              
264             =head2 has_option(option)
265              
266             Returns whether this element has a child option with the provided name, provided this object is a select.
267              
268             B:
269             I
270              
271             B:
272             I - whether this object has said option as a child
273              
274             =cut
275              
276             #Convenience method for selects
277             sub has_option {
278 0     0 1   my ($self,$option) = @_;
279 0 0         confess("Object parameters must be called by an instance") unless ref($self);
280 0 0         confess("Option must be passed as argument") unless defined($option);
281 0 0         return 0 if !$self->is_select();
282 0           return scalar(grep {$_->name eq $option} $self->get_options());
  0            
283             }
284              
285             =head2 is_selected
286              
287             Returns whether the element is selected.
288              
289             =cut
290              
291             sub is_selected {
292 0     0 1   my $self = shift;
293 0 0         confess("Object parameters must be called by an instance") unless ref($self);
294 0 0         confess("Element must be option to check if selected") unless $self->is_option;
295 0 0         return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'},'selected') : $self->{'element'}->get_attribute('selected');
296             }
297              
298             =head2 get
299              
300             Returns the current value of the element.
301              
302             B:
303              
304             I - Depends on the type of element.
305             Boolean for checkboxes, options and radiobuttons
306             Arrayrefs of option names for multi-selects
307             Strings for single selects, text/hidden inputs and non-inputs like paragraphs, table cells, etc.
308              
309             =cut
310              
311             sub get {
312 0     0 1   my ($self) = @_;
313 0 0         confess("Object parameters must be called by an instance") unless ref($self);
314              
315 0           my $ret = 0;
316              
317             #Try to get various stuff based on what it is
318 0 0 0       if ($self->is_checkbox || $self->is_radio) {
    0 0        
    0 0        
    0          
319 0 0         return $self->{'driver'} ? $self->{'driver'}->is_checked() : $self->{'element'}->is_selected();
320             } elsif ($self->is_select) {
321 0 0         if ($self->is_multiselect) {
322 0 0         my @options = grep {defined $_} map {$_->is_selected ? $_->name : undef} $self->get_options;
  0            
  0            
323 0           return \@options;
324             } else {
325 0 0         return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'},'value') : $self->{'element'}->get_attribute('value');
326             }
327             } elsif ( $self->is_hiddeninput || $self->is_fileinput || $self->is_textinput) {
328 0 0         return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'},'value') : $self->{'element'}->get_attribute('value');
329             } elsif ($self->is_option) {
330 0 0         return $self->{'driver'} ? defined $self->{'driver'}->get_attribute($self->{'element'},'selected') : defined $self->{'element'}->get_attribute('selected');
331             } else {
332 0 0         $self->{'driver'} ? $self->{'driver'}->get_text($self->{'element'}) : $self->{'element'}->get_text();
333             }
334             }
335              
336             =head2 id
337              
338             Returns the element's id.
339              
340             =cut
341              
342             sub id {
343 0     0 1   my $self = shift;
344 0 0         return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'},'id') : $self->{'element'}->get_attribute('id');
345             }
346              
347             =head2 name
348              
349             Returns the element's name.
350              
351             =cut
352              
353             sub name {
354 0     0 1   my $self = shift;
355 0 0         return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'},'name') : $self->{'element'}->get_attribute('name');
356             }
357              
358             =head1 SETTERS
359              
360             =head2 clear
361              
362             Clear a text input.
363              
364             =cut
365              
366             sub clear {
367 0     0 1   my ($self) = @_;
368 0 0         confess("Object parameters must be called by an instance") unless ref($self);
369 0 0         confess("Element must be text") unless $self->is_textinput();
370 0 0         if ($self->{'driver'}) {
371             #TODO If you can't do it with both of these, you have no business doing it...but this could be expanded to everything eventually...
372 0 0         confess('WWW::Selenium drivers can only clear text if selector is of type "id" or "css"') unless scalar(grep {$_ eq $self->{'selector'}->[1]} qw(id css));
  0            
373 0 0         my $js = $self->{'selector'}->[1] eq 'id' ? 'document.getElementById("'.$self->{'selector'}->[0].'").value = ""' : 'document.querySelectorAll("'.$self->{'selector'}->[0].'")[0].value = ""';
374 0           $self->javascript($js);
375             } else {
376 0           $self->{'element'}->clear();
377             }
378 0           return 1;
379             }
380              
381             =head2 set(value,[callback])
382              
383             Set the value of the input to the provided value, and execute the provided callback if provided.
384             The callback will be provided with the caller and the selenium driver as arguments.
385              
386             B:
387              
388             I - STRING, BOOLEAN or ARRAYREF, depending on the type of element you are attempting to set.
389             Strings are for textinputs, hiddens or non-multi selects, Booleans for radiobuttons, checkboxes and options, and Arrayrefs of strings for multiselects.
390             Selects take the name of the option as arguments.
391              
392             I - some anonymous function
393              
394             B:
395              
396             I - whether the set succeeded, or whatever your callback feels like returning, supposing you provided one.
397              
398             =cut
399              
400             sub set {
401 0     0 1   my ($self,$value,$callback) = @_;
402 0 0         confess("Object parameters must be called by an instance") unless ref($self);
403 0 0         confess("Value must be passed to set") unless defined($value);
404 0 0 0       confess("Callback must be subroutine") if defined($callback) && reftype($callback) ne 'CODE';
405              
406 0           my $enabled = $self->is_enabled();
407 0 0         carp "Attempting to set disabled element" unless $enabled;
408 0 0         return undef unless $enabled;
409 0           my $ret = 0;
410              
411             #Try to set various stuff based on what it is
412             SETBLOCK : {
413 0 0 0       if ($self->is_checkbox || $self->is_radio) {
  0 0          
    0          
    0          
    0          
    0          
414 0 0         my $selected = $self->{'driver'} ? $self->{'driver'}->is_checked() : $self->{'element'}->is_selected();
415 0 0 0       last SETBLOCK if ($selected && $value) || (!$selected && !$value); #Return false if state hasn't changed
      0        
      0        
416 0 0         $self->{'driver'} ? $self->{'driver'}->click($self->{'element'}) : $self->{'element'}->click();
417 0           $ret = 1;
418             } elsif ($self->is_textinput) {
419 0           $self->clear();
420 0 0         $self->{'driver'} ? $self->{'driver'}->type_keys($self->{'element'},$value) : $self->{'element'}->send_keys($value);
421 0           $ret = 1;
422             } elsif ($self->is_fileinput) {
423 0 0         if ($self->{'driver'}) {
424 0           $self->{'driver'}->attach_file($self->{'element'},$value);
425             } else {
426 0           $self->{'element'}->send_keys($value);
427             }
428 0           $ret = 1;
429             } elsif ($self->is_hiddeninput) {
430             #TODO make this work a bit more universally if possible
431 0 0         confess("Setting values on hidden elements without IDs not supported") unless $self->id;
432 0           carp("Setting value of hidden element, this may result in unexpected behavior!");
433 0           my $js = 'document.getElementById("'.$self->id.'").value = \''.$value.'\';';
434 0           $self->javascript($js);
435 0           $ret = 1;
436             } elsif ($self->is_select) {
437 0 0         $value = [$value] if reftype($value) ne 'ARRAY';
438 0 0         if ($self->{'driver'}) {
439 0           foreach my $val (@$value) {
440 0           $self->{'driver'}->type($self->{'element'},$value);
441             }
442             } else {
443 0           foreach my $val ($self->get_options()) {
444 0 0         if (grep {$val->{'element'}->get_attribute('name') eq $_ } @$value) {
  0            
445             #Leave values high if they are requested
446 0 0         $val->click if !$val->is_selected;
447             } else {
448             #otherwise ensure low values
449 0 0         $val->click if $val->is_selected;
450             }
451             }
452             }
453 0           $ret = 1;
454             } elsif ($self->is_option) {
455 0           my $current = $self->get;
456 0 0 0       $self->click if ( (!$current && $value) || ($current && !$value) );
      0        
      0        
457             } else {
458 0           confess("Don't know how to set value to a non-input element!");
459             }
460             }
461              
462             #Can't set anything else!
463 0   0       return $self->_doCallback($callback) || $ret;
464             }
465              
466             sub _doCallback {
467 0     0     my ($self,$cb) = @_;
468 0 0         return 0 if !$cb;
469 0 0         return &$cb($self,$self->{'driver'} ? $self->{'driver'} : $self->{'element'}->{'driver'});
470             }
471              
472             =head2 randomize(options)
473              
474             Randomizes the input, depending on the type of element. Useful for fuzzing.
475              
476             B:
477              
478             I: Options appropraite to the relevant Data::Random method.
479              
480             B:
481              
482             I - Random value that has been set into the field, or false on failure.
483              
484             =cut
485              
486 0     0 1   sub randomize {
487              
488             }
489              
490             =head1 STATE CHANGE METHODS
491              
492             =head2 javascript(js)
493              
494             Execute an arbitrary Javascript string and return the output.
495             Handy in callbacks that wait for JS events.
496              
497             B:
498              
499             I - any valid javascript string
500              
501             B:
502              
503             I - depends on your javascript's output.
504              
505             =cut
506              
507             sub javascript {
508 0     0 1   my ($self, $js) = @_;
509 0 0         confess("Object parameters must be called by an instance") unless ref($self);
510 0 0         return $self->{'driver'} ? $self->{'element'}->get_eval($js) : $self->{'element'}->{'driver'}->execute_script($js);
511             }
512              
513             =head2 click
514              
515             Click the element.
516              
517             =cut
518              
519             sub click {
520 0     0 1   my ($self,$callback) = @_;
521 0 0         confess("Object parameters must be called by an instance") unless ref($self);
522 0 0 0       confess("Callback must be subroutine") if defined($callback) && reftype($callback) ne 'CODE';
523 0 0         $self->{'driver'} ? $self->{'driver'}->click($self->{'element'}) : $self->{'element'}->click();
524              
525 0   0       return $self->_doCallback($callback) || 1;
526             }
527              
528             =head2 submit([callback])
529              
530             Submit the element, supposing it's a form
531              
532             B:
533              
534             I - anonymous function
535              
536             B:
537              
538             I - Whether the action succeeded or whatever your callback returns, supposing it was provided.
539              
540             =cut
541              
542             sub submit {
543 0     0 1   my ($self,$callback) = @_;
544 0 0         confess("Object parameters must be called by an instance") unless ref($self);
545 0 0 0       confess("Callback must be subroutine") if defined($callback) && reftype($callback) ne 'CODE';
546 0 0         return 0 if !$self->is_form();
547 0 0         $self->{'driver'} ? $self->{'driver'}->submit($self->{'element'}) : $self->{'element'}->submit();
548              
549 0   0       return $self->_doCallback($callback) || 1;
550             }
551              
552             1;
553              
554             __END__