File Coverage

blib/lib/Selenium/Element.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Selenium::Element;
2              
3 1     1   4 use strict;
  1         2  
  1         30  
4 1     1   4 use warnings;
  1         2  
  1         21  
5              
6 1     1   4 use Carp;
  1         1  
  1         53  
7 1     1   4 use Scalar::Util qw(blessed reftype looks_like_number);
  1         2  
  1         55  
8 1     1   1808 use Data::Random;
  0            
  0            
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             my ($class,$element,$driver,$selector) = @_;
42             confess("Constructor must be called statically, not by an instance") if ref($class);
43             return undef if !$element;
44             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' );
45              
46             my $self = {
47             'driver' => $driver,
48             'element' => $element,
49             'selector' => $selector
50             };
51              
52             bless $self, $class;
53             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             my ($self) = @_;
66             confess("Object parameters must be called by an instance") unless ref($self);
67             if ($self->{'driver'}) {
68             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             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));
71             my $js = $parts[0] eq 'id' ? 'document.getElementById("'.$parts[1].'").nodeName' : 'document.querySelectorAll("'.$parts[1].'")[0].nodeName';
72             return lc($self->javascript($js));
73             }
74             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             my $self = shift;
85             confess("Object parameters must be called by an instance") unless ref($self);
86             return undef unless $self->is_input;
87             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             my ($self) = @_;
101             confess("Object parameters must be called by an instance") unless ref($self);
102             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             my ($self) = @_;
113             confess("Object parameters must be called by an instance") unless ref($self);
114             my $itype = $self->get_type();
115             my $ret = scalar(grep {$_ eq $itype} ('password', 'text'));
116             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             my ($self) = @_;
127             confess("Object parameters must be called by an instance") unless ref($self);
128             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             my $self = shift;
139             confess("Object parameters must be called by an instance") unless ref($self);
140             return 0 if !$self->is_select;
141             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             my ($self) = @_;
152             confess("Object parameters must be called by an instance") unless ref($self);
153             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             my ($self) = @_;
164             confess("Object parameters must be called by an instance") unless ref($self);
165             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             my ($self) = @_;
176             confess("Object parameters must be called by an instance") unless ref($self);
177             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             my ($self) = @_;
188             confess("Object parameters must be called by an instance") unless ref($self);
189             return $self->get_type() eq 'file';
190             }
191              
192             =head2 is_fileinput
193              
194             Returns whether the element is an input of the type 'file'.
195              
196             =cut
197              
198             sub is_form {
199             my ($self) = @_;
200             confess("Object parameters must be called by an instance") unless ref($self);
201             confess("WWW::Selenium does not support getting tag type of elements") if $self->{'driver'};
202             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             my ($self) = @_;
213             confess("Object parameters must be called by an instance") unless ref($self);
214             confess("WWW::Selenium does not support getting tag type of elements") if $self->{'driver'};
215             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             my $self = shift;
226             confess("Object parameters must be called by an instance") unless ref($self);
227             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             my ($self) = @_;
238             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             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             my $self = shift;
251             confess("Object parameters must be called by an instance") unless ref($self);
252             return () unless $self->is_select();
253             my @options = ();
254             if ($self->{'driver'}) {
255             #XXX obviously not reliable
256             carp("WARNING: WWW::Selenium has reduced ability to get options! This may not work as you expect.");
257             my @labels = $self->{'driver'}->get_select_options($self->{'element'});
258             return map {Selenium::Element->new("css=option[value=$_]",$self->{'driver'})} @labels;
259             }
260             my @opts = $self->{'element'}->{'driver'}->find_child_elements($self->{'element'},'option','tag_name');
261             return map {Selenium::Element->new($_,0)} @opts;
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             my ($self,$option) = @_;
279             confess("Object parameters must be called by an instance") unless ref($self);
280             confess("Option must be passed as argument") unless defined($option);
281             return 0 if !$self->is_select();
282             return scalar(grep {$_->name eq $option} $self->get_options());
283             }
284              
285             =head2 is_selected
286              
287             Returns whether the element is selected.
288              
289             =cut
290              
291             sub is_selected {
292             my $self = shift;
293             confess("Object parameters must be called by an instance") unless ref($self);
294             confess("Element must be option to check if selected") unless $self->is_option;
295             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             my ($self) = @_;
313             confess("Object parameters must be called by an instance") unless ref($self);
314              
315             my $ret = 0;
316              
317             #Try to get various stuff based on what it is
318             if ($self->is_checkbox || $self->is_radio) {
319             return $self->{'driver'} ? $self->{'driver'}->is_checked() : $self->{'element'}->is_selected();
320             } elsif ($self->is_select) {
321             if ($self->is_multiselect) {
322             my @options = grep {defined $_} map {$_->is_selected ? $_->name : undef} $self->get_options;
323             return \@options;
324             } else {
325             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             return $self->{'driver'} ? $self->{'driver'}->get_attribute($self->{'element'},'value') : $self->{'element'}->get_attribute('value');
329             } elsif ($self->is_option) {
330             return $self->{'driver'} ? defined $self->{'driver'}->get_attribute($self->{'element'},'selected') : defined $self->{'element'}->get_attribute('selected');
331             } else {
332             $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             my $self = shift;
344             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             my $self = shift;
355             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             my ($self) = @_;
368             confess("Object parameters must be called by an instance") unless ref($self);
369             confess("Element must be text") unless $self->is_textinput();
370             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             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));
373             my $js = $self->{'selector'}->[1] eq 'id' ? 'document.getElementById("'.$self->{'selector'}->[0].'").value = ""' : 'document.querySelectorAll("'.$self->{'selector'}->[0].'")[0].value = ""';
374             $self->javascript($js);
375             } else {
376             $self->{'element'}->clear();
377             }
378             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             my ($self,$value,$callback) = @_;
402             confess("Object parameters must be called by an instance") unless ref($self);
403             confess("Value must be passed to set") unless defined($value);
404             confess("Callback must be subroutine") if defined($callback) && reftype($callback) ne 'CODE';
405              
406             my $enabled = $self->is_enabled();
407             carp "Attempting to set disabled element" unless $enabled;
408             return undef unless $enabled;
409             my $ret = 0;
410              
411             #Try to set various stuff based on what it is
412             SETBLOCK : {
413             if ($self->is_checkbox || $self->is_radio) {
414             my $selected = $self->{'driver'} ? $self->{'driver'}->is_checked() : $self->{'element'}->is_selected();
415             last SETBLOCK if ($selected && $value) || (!$selected && !$value); #Return false if state hasn't changed
416             $self->{'driver'} ? $self->{'driver'}->click($self->{'element'}) : $self->{'element'}->click();
417             $ret = 1;
418             } elsif ($self->is_textinput) {
419             $self->clear();
420             $self->{'driver'} ? $self->{'driver'}->type_keys($self->{'element'},$value) : $self->{'element'}->send_keys($value);
421             $ret = 1;
422             } elsif ($self->is_fileinput) {
423             if ($self->{'driver'}) {
424             $self->{'driver'}->attach_file($self->{'element'},$value);
425             } else {
426             $self->{'element'}->send_keys($value);
427             }
428             $ret = 1;
429             } elsif ($self->is_hiddeninput) {
430             #TODO make this work a bit more universally if possible
431             confess("Setting values on hidden elements without IDs not supported") unless $self->id;
432             carp("Setting value of hidden element, this may result in unexpected behavior!");
433             my $js = 'document.getElementById("'.$self->id.'").value = \''.$value.'\';';
434             $self->javascript($js);
435             $ret = 1;
436             } elsif ($self->is_select) {
437             $value = [$value] if reftype($value) ne 'ARRAY';
438             if ($self->{'driver'}) {
439             foreach my $val (@$value) {
440             $self->{'driver'}->type($self->{'element'},$value);
441             }
442             } else {
443             foreach my $val ($self->get_options()) {
444             if (grep {$val->{'element'}->get_attribute('name') eq $_ } @$value) {
445             #Leave values high if they are requested
446             $val->click if !$val->is_selected;
447             } else {
448             #otherwise ensure low values
449             $val->click if $val->is_selected;
450             }
451             }
452             }
453             $ret = 1;
454             } elsif ($self->is_option) {
455             my $current = $self->get;
456             $self->click if ( (!$current && $value) || ($current && !$value) );
457             } else {
458             confess("Don't know how to set value to a non-input element!");
459             }
460             }
461              
462             #Can't set anything else!
463             return $self->_doCallback($callback) || $ret;
464             }
465              
466             sub _doCallback {
467             my ($self,$cb) = @_;
468             return 0 if !$cb;
469             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             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             my ($self, $js) = @_;
509             confess("Object parameters must be called by an instance") unless ref($self);
510             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             my ($self,$callback) = @_;
521             confess("Object parameters must be called by an instance") unless ref($self);
522             confess("Callback must be subroutine") if defined($callback) && reftype($callback) ne 'CODE';
523             $self->{'driver'} ? $self->{'driver'}->click($self->{'element'}) : $self->{'element'}->click();
524              
525             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             my ($self,$callback) = @_;
544             confess("Object parameters must be called by an instance") unless ref($self);
545             confess("Callback must be subroutine") if defined($callback) && reftype($callback) ne 'CODE';
546             return 0 if !$self->is_form();
547             $self->{'driver'} ? $self->{'driver'}->submit($self->{'element'}) : $self->{'element'}->submit();
548              
549             return $self->_doCallback($callback) || 1;
550             }
551              
552             1;
553              
554             __END__