File Coverage

blib/lib/Weasel/Session.pm
Criterion Covered Total %
statement 78 150 52.0
branch 10 26 38.4
condition 4 12 33.3
subroutine 24 55 43.6
pod 18 18 100.0
total 134 261 51.3


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             Weasel::Session - Connection to an encapsulated test driver
5              
6             =head1 VERSION
7              
8             0.30
9              
10             =head1 SYNOPSIS
11              
12             use Weasel;
13             use Weasel::Session;
14             use Weasel::Driver::Selenium2;
15              
16             my $weasel = Weasel->new(
17             default_session => 'default',
18             sessions => {
19             default => Weasel::Session->new(
20             driver => Weasel::Driver::Selenium2->new(%opts),
21             ),
22             });
23              
24             $weasel->session->get('http://localhost/index');
25              
26              
27             =head1 DESCRIPTION
28              
29              
30              
31             =cut
32              
33             =head1 DEPENDENCIES
34              
35              
36              
37             =cut
38              
39             package Weasel::Session;
40              
41              
42 2     2   2234 use strict;
  2         6  
  2         65  
43 2     2   11 use warnings;
  2         8  
  2         60  
44              
45 2     2   11 use Moose;
  2         4  
  2         18  
46 2     2   10009 use namespace::autoclean;
  2         4  
  2         21  
47              
48 2     2   1457 use HTML::Selector::XPath;
  2         6054  
  2         119  
49 2     2   30 use Module::Runtime qw/ use_module /;;
  2         5  
  2         17  
50 2     2   969 use Weasel::FindExpanders qw/ expand_finder_pattern /;
  2         21  
  2         139  
51 2     2   929 use Weasel::WidgetHandlers qw| best_match_handler_class |;
  2         5  
  2         5494  
52              
53             our $VERSION = '0.30';
54              
55             our $MINIMUM_DRIVER_VERSION = '0.03';
56              
57             =head1 ATTRIBUTES
58              
59             =over
60              
61             =item driver
62              
63             Holds a reference to the sessions's driver.
64              
65             =cut
66              
67             has 'driver' => (is => 'ro',
68             required => 1,
69             handles => {
70             '_start' => 'start',
71             'stop' => 'stop',
72             '_restart' => 'restart',
73             'started' => 'started',
74             },
75             );
76              
77             =item widget_groups
78              
79             Contains the list of widget groups to be used with the session, or
80             uses all groups when undefined.
81              
82             Note: this functionality allows one to load multiple groups into the running
83             perl instance, while using different groups in various sessions.
84              
85             =cut
86              
87             has 'widget_groups' => (is => 'rw');
88              
89             =item base_url
90              
91             Holds the prefix that will be prepended to every URL passed
92             to this API.
93             The prefix can be an environment variable, e.g. ${VARIABLE}.
94             It will be expanded and default to hppt://localhost:5000 if not defined.
95             If it is not an environment variable, it will be used as is.
96              
97             =cut
98              
99             has 'base_url' => (is => 'rw',
100             isa => 'Str',
101             default => '',
102             );
103              
104             =item page
105              
106             Holds the root element of the target HTML page (the 'html' tag).
107              
108             =cut
109              
110             has 'page' => (is => 'ro',
111             isa => 'Weasel::Element::Document',
112             builder => '_build_page',
113             lazy => 1,
114             );
115              
116             sub _build_page {
117 1     1   3 my $self = shift;
118 1         36 my $class = use_module($self->page_class);
119              
120 1         41 return $class->new(session => $self);
121             }
122              
123             =item log_hook
124              
125             Upon instantiation can be set to log consumer; a function of 3 arguments:
126             1. the name of the event
127             2. the text to be logged (or a coderef to be called without arguments returning such)
128              
129             =cut
130              
131             has 'log_hook' => (is => 'ro',
132             isa => 'Maybe[CodeRef]',
133             );
134              
135             =item page_class
136              
137             Upon instantiation can be set to an alternative class name for the C<page>
138             attribute.
139              
140             =cut
141              
142             has 'page_class' => (is => 'ro',
143             isa => 'Str',
144             default => 'Weasel::Element::Document',
145             );
146              
147             =item retry_timeout
148              
149             The number of seconds to poll for a condition to become true. Global
150             setting for the C<wait_for> function.
151              
152             =cut
153              
154             has 'retry_timeout' => (is => 'rw',
155             default => 15,
156             isa => 'Num',
157             );
158              
159             =item poll_delay
160              
161             The number of seconds to wait between state polling attempts. Global
162             setting for the C<wait_for> function.
163              
164             =cut
165              
166             has 'poll_delay' => (is => 'rw',
167             default => 0.5,
168             isa => 'Num',
169             );
170              
171              
172             =item state
173              
174             Holds one of
175              
176             =over
177              
178             =item * initial
179              
180             =item * started
181              
182             =item * stopped
183              
184             =back
185              
186             Before the first page is loaded into the browser, the value of the
187             C<state> property is C<initial>. After the first C<get> call, the
188             value changes to C<started>.
189              
190             =cut
191              
192             has 'state' => (is => 'rw',
193             default => 'initial',
194             isa => 'Str');
195              
196             =back
197              
198             =head1 SUBROUTINES/METHODS
199              
200              
201             =over
202              
203             =item clear($element)
204              
205             Clears any input entered into elements supporting it. Generally applies to
206             textarea elements and input elements of type text and password.
207              
208             =cut
209              
210             sub clear {
211 0     0 1 0 my ($self, $element) = @_;
212              
213 0     0   0 return $self->_logged(sub { $self->driver->clear($element->_id); },
214 0         0 'clear', 'clearing input element');
215             }
216              
217             =item click([$element])
218              
219             Simulates a single mouse click. If an element argument is provided, that
220             element is clicked. Otherwise, the browser window is clicked at the
221             current mouse location.
222              
223             =cut
224              
225             sub click {
226 0     0 1 0 my ($self, $element) = @_;
227              
228             return $self->_logged(
229             sub {
230 0 0   0   0 $self->driver->click(($element) ? $element->_id : undef);
231             },
232 0 0       0 'click', ($element) ? 'clicking element' : 'clicking window');
233             }
234              
235             =item find($element, $locator [, scheme => $scheme] [, widget_args => \@args ] [, %locator_args])
236              
237             Finds the first child of C<$element> matching C<$locator>.
238              
239             See L<Weasel::Element>'s C<find> function for more documentation.
240              
241             =cut
242              
243             sub find {
244 0     0 1 0 my ($self, @args) = @_;
245 0         0 my $rv;
246              
247             $self->_logged(
248             sub {
249             $self->wait_for(
250             sub {
251 0         0 my @rv = @{$self->find_all(@args)};
  0         0  
252 0         0 return $rv = shift @rv;
253 0     0   0 });
254 0         0 }, 'find', 'find ' . $args[1]);
255              
256 0         0 return $rv;
257             }
258              
259             =item find_all($element, $locator, [, scheme => $scheme] [, widget_args => \@args ] [, %locator_args ])
260              
261             Finds all child elements of C<$element> matching C<$locator>. Returns,
262             depending on scalar or list context, an arrayref or a list with matching
263             elements.
264              
265             See L<Weasel::Element>'s C<find_all> function for more documentation.
266              
267             =cut
268              
269             sub find_all {
270 2     2 1 9 my ($self, $element, $pattern, %args) = @_;
271              
272 2         2 my $expanded_pattern;
273             # if (exists $args{scheme} and $args{scheme} eq 'css') {
274             # delete $args{scheme};
275             # $expanded_pattern =
276             # q{.} . HTML::Selector::XPath->new($pattern)->to_xpath;
277             # }
278             # else {
279 2         9 $expanded_pattern = expand_finder_pattern($pattern, \%args);
280             # }
281             my @rv = $self->_logged(
282             sub {
283             return
284 4         29 map { $self->_wrap_widget($_, $args{widget_args}) }
285             $self->driver->find_all($element->_id,
286             $expanded_pattern,
287 2     2   60 $args{scheme});
288             },
289             'find_all',
290             sub {
291 2     2   5 my ($rv) = @_;
292             ##no critic(ProhibitUselessTopic)
293 2         11 return 'found ' . scalar(@{$rv}) . " elements for $pattern "
294             . (join ', ', %args) . "\n"
295             . (join "\n",
296 4         36 map { ' - ' . ref($_)
297 2         3 . ' (' . $_->tag_name . ')' } @{$rv});
  2         5  
298             },
299 2         18 "pattern: $pattern");
300 2 100       21 return wantarray ? @rv : \@rv;
301             }
302              
303              
304             =item get($url)
305              
306             Loads C<$url> into the active browser window of the driver connection,
307             after prefixing with C<base_url>.
308              
309             =cut
310              
311             sub get {
312 0     0 1 0 my ($self, $url) = @_;
313              
314             my $base = $self->base_url =~ /\$\{(\w+)\}/x
315 0 0 0     0 ? $ENV{$1} // 'http://localhost:5000'
316             : $self->base_url;
317 0         0 $url = $base . $url;
318 0         0 $self->state('started');
319             ###TODO add logging warning of urls without protocol part
320             # which might indicate empty 'base_url' where one is assumed to be set
321             return $self->_logged(
322             sub {
323 0     0   0 return $self->driver->get($url);
324 0         0 }, 'get', "loading URL: $url");
325             }
326              
327             =item get_attribute($element, $attribute)
328              
329             Returns the value of the attribute named by C<$attribute> of the element
330             identified by C<$element>, or C<undef> if the attribute isn't defined.
331              
332             =cut
333              
334             sub get_attribute {
335 0     0 1 0 my ($self, $element, $attribute) = @_;
336              
337             return $self->_logged(
338             sub {
339 0     0   0 return $self->driver->get_attribute($element->_id, $attribute);
340 0         0 }, 'get_attribute', "element attribute '$attribute'");
341             }
342              
343             =item get_text($element)
344              
345             Returns the 'innerHTML' of the element identified by C<$element>.
346              
347             =cut
348              
349             sub get_text {
350 0     0 1 0 my ($self, $element) = @_;
351              
352             return $self->_logged(
353             sub {
354 0     0   0 return $self->driver->get_text($element->_id);
355             },
356 0         0 'get_text', 'element text');
357             }
358              
359             =item set_attribute($element_id, $attribute_name, $value)
360              
361             DEPRECATED
362              
363             Changes the value of the attribute named by C<$attribute_name> to C<$value>
364             for the element identified by C<$element_id>.
365              
366             =cut
367              
368             sub set_attribute {
369 0     0 1 0 my ($self, $element, $attribute, $value) = @_;
370              
371             return $self->_logged(
372             sub {
373 0     0   0 return $self->driver->set_attribute($element->_id,
374             $attribute, $value);
375             },
376 0         0 'set_attribute', qq{Setting attribute $attribute to '$value'});
377             }
378              
379             =item get_selected($element_id)
380              
381             DEPRECATED
382              
383             Please use C<$self->get_attribute('selected')> instead.
384              
385             =cut
386              
387             sub get_selected {
388 0     0 1 0 my ($self, $element) = @_;
389              
390             return $self->_logged(
391             sub {
392 0     0   0 return $self->driver->get_selected($element->_id);
393             },
394 0         0 'get_selected', 'Is element selected?');
395             }
396              
397             =item set_selected($element_id, $value)
398              
399             DEPRECATED
400              
401             Please use C<$self->set_attribute('selected', $value)> instead.
402              
403             =cut
404              
405             sub set_selected {
406 0     0 1 0 my ($self, $element, $value) = @_;
407              
408             return $self->_logged(
409             sub {
410 0     0   0 return $self->driver->get_selected($element->_id, $value);
411             },
412 0         0 'set_selected', qq{Setting 'selected' property: $value});
413             }
414              
415              
416             =item is_displayed($element)
417              
418             Returns a boolean value indicating if the element identified by
419             C<$element> is visible on the page, i.e. that it can be scrolled into
420             the viewport for interaction.
421              
422             =cut
423              
424             sub is_displayed {
425 0     0 1 0 my ($self, $element) = @_;
426              
427             return $self->_logged(
428             sub {
429 0     0   0 return $self->driver->is_displayed($element->_id);
430             },
431 0         0 'is_displayed', 'query is_displayed');
432             }
433              
434             =item screenshot($fh)
435              
436             Writes a screenshot of the browser's window to the filehandle C<$fh>.
437              
438             Note: this version assumes pictures of type PNG will be written;
439             later versions may provide a means to query the exact image type of
440             screenshots being generated.
441              
442             =cut
443              
444             sub screenshot {
445 1     1 1 13 my ($self, $fh) = @_;
446              
447             return $self->_logged(
448             sub {
449 1     1   27 $self->driver->screenshot($fh);
450 1         6 }, 'screenshot', 'screenshot');
451             }
452              
453             =item start
454              
455             Starts a new or stopped session.
456              
457             Sets C<state> back to the value C<initial>.
458              
459             =item restart
460              
461              
462             Restarts a session by resetting it and starting.
463              
464             Sets C<state> back to the value C<initial>.
465              
466             =item stop
467              
468             =item started
469              
470             Returns a C<true> value when the session has been started.
471              
472             =cut
473              
474             sub start {
475 0     0 1 0 my $self = shift;
476 0         0 $self->_start;
477 0         0 $self->state('initial');
478             }
479              
480             sub restart {
481 0     0 1 0 my $self = shift;
482 0         0 $self->_restart;
483 0         0 $self->state('initial');
484             }
485              
486             =item get_page_source($fh)
487              
488             Writes a get_page_source of the browser's window to the filehandle C<$fh>.
489              
490             =cut
491              
492             sub get_page_source {
493 0     0 1 0 my ($self,$fh) = @_;
494              
495             return $self->_logged(
496             sub {
497 0     0   0 $self->driver->get_page_source($fh);
498 0         0 }, 'get_page_source', 'get_page_source');
499             }
500              
501             =item send_keys($element, @keys)
502              
503             Send the characters specified in the strings in C<@keys> to C<$element>,
504             simulating keyboard input.
505              
506             =cut
507              
508             sub send_keys {
509 0     0 1 0 my ($self, $element, @keys) = @_;
510              
511             return $self->_logged(
512             sub {
513 0     0   0 $self->driver->send_keys($element->_id, @keys);
514             },
515 0   0     0 'send_keys', 'sending keys: ' . (join '', @keys // ()));
516             }
517              
518             =item tag_name($element)
519              
520             Returns the tag name of the element identified by C<$element>.
521              
522             =cut
523              
524             sub tag_name {
525 4     4 1 10 my ($self, $element) = @_;
526              
527 4     4   88 return $self->_logged(sub { return $self->driver->tag_name($element->_id) },
528             'tag_name',
529 0     0   0 sub { my $tag = shift;
530 0 0       0 return ($tag)
531             ? "found tag with name '$tag'" : 'no tag name found' },
532 4         36 'getting tag name');
533             }
534              
535             =item wait_for($callback, [ retry_timeout => $number,] [poll_delay => $number,] [ on_timeout => \&cb ])
536              
537             Polls $callback->() until it returns true, or C<wait_timeout> expires
538             -- whichever comes first.
539              
540             The arguments retry_timeout and poll_delay can be used to override the
541             session-global settings.
542              
543             =cut
544              
545             sub _wrap_callback {
546 0     0   0 my ($self, $cb) = @_;
547              
548 0 0       0 if (! $self->log_hook) {
549 0         0 return $cb;
550             }
551             else {
552 0         0 my $count = 0;
553             return sub {
554 0 0   0   0 if ($count) {
555 0         0 my $log_hook = $self->log_hook;
556 0         0 local $self->{log_hook} = undef; # suppress logging
557 0         0 my $rv = $cb->();
558 0 0       0 if ($rv) {
559             # $self->log_hook is still bound to 'undef'
560 0         0 $log_hook->('post_wait_for',
561             "success after $count retries");
562             }
563 0         0 $count++;
564 0         0 return $rv;
565             }
566             else {
567 0         0 $count++;
568 0         0 $self->log_hook->('pre_wait_for',
569             'checking wait_for conditions');
570 0         0 return $cb->();
571             }
572 0         0 };
573             }
574             }
575              
576             sub wait_for {
577 0     0 1 0 my ($self, $callback, %args) = @_;
578              
579             return $self->_logged(
580             sub {
581 0     0   0 $self->driver->wait_for($self->_wrap_callback($callback),
582             retry_timeout => $self->retry_timeout,
583             poll_delay => $self->poll_delay,
584             %args);
585             },
586 0         0 'wait_for', 'waiting for condition');
587             }
588              
589              
590             before 'BUILDARGS', sub {
591             my ($class, @args) = @_;
592             my $args = (ref $args[0]) ? $args[0] : { @args };
593              
594             confess "Driver used to construct session object uses old API version;\n" .
595             'some functionality may not work correctly'
596             if ($args->{driver}
597             && $args->{driver}->implements < $MINIMUM_DRIVER_VERSION);
598             };
599              
600             sub _appending_wrap {
601 4     4   7 my ($str) = @_;
602             return sub {
603 4     4   21 my $rv = shift;
604 4 50       14 if ($rv) {
605 0         0 return "$str ($rv)";
606             }
607             else {
608 4         13 return $str;
609             }
610             }
611 4         27 }
612              
613             =item _logged($wrapped_fn, $event, $log_item, $log_item_pre)
614              
615             Invokes C<log_hook> when it's defined, before and after calling C<$wrapped_fn>
616             with no arguments, with the 'pre_' and 'post_' prefixes to the event name.
617              
618             C<$log_item> can be a fixed string or a function of one argument returning
619             the string to be logged. The argument passed into the function is the value
620             returned by the C<$wrapped_fn>.
621              
622             In case there is no C<$log_item_pre> to be called on the 'pre_' event,
623             C<$log_item> will be used instead, with no arguments.
624              
625             For performance reasons, the C<$log_item> and C<$log_item_pre> - when
626             coderefs - aren't called; instead they are passed as-is to the
627             C<$log_hook> for lazy evaluation.
628              
629             =cut
630              
631             sub _unlogged {
632 6     6   10 my ($self, $func) = @_;
633              
634 6         43 local $self->{log_hook} = undef;
635 6         39 $func->();
636              
637 6         53 return;
638             }
639              
640             sub _logged {
641 7     7   19 my ($self, $f, $e, $l, $lp) = @_;
642 7         175 my $hook = $self->log_hook;
643              
644 7 100       27 return $f->() if ! defined $hook;
645              
646 3   66     11 $lp //= $l;
647 3 50       11 my $pre = (ref $lp eq 'CODE') ? $lp : _appending_wrap($lp);
648 3 100       14 my $post = (ref $l eq 'CODE') ? $l : _appending_wrap($l);
649             $self->_unlogged(
650 3     3   14 sub { $hook->("pre_$e", $pre); }
651 3         15 );
652 3 100       15 if (wantarray) {
653 2         6 my @rv = $f->();
654             $self->_unlogged(
655 2     2   9 sub { $hook->("post_$e", sub { return $post->(\@rv); }); }
  2         12  
656 2         15 );
657 2         15 return @rv;
658             }
659             else {
660 1         5 my $rv = $f->();
661             $self->_unlogged(
662 1     1   9 sub { $hook->("post_$e", sub { return $post->($rv); }); }
  1         6  
663 1         14 );
664 1         8 return $rv;
665             }
666             };
667              
668             =item _wrap_widget($_id)
669              
670             Finds all matching widget selectors to wrap the driver element in.
671              
672             In case of multiple matches, selects the most specific match
673             (the one with the highest number of requirements).
674              
675             =cut
676              
677             sub _wrap_widget {
678 4     4   13 my ($self, $_id, $widget_args) = @_;
679 4   50     121 my $best_class = best_match_handler_class(
680             $self->driver, $_id, $self->widget_groups) // 'Weasel::Element';
681 4   50     17 $widget_args //= [];
682 4         6 return $best_class->new(_id => $_id, session => $self, @{$widget_args});
  4         104  
683             }
684              
685             =back
686              
687             =head1 SEE ALSO
688              
689             L<Weasel>
690              
691             =head1 AUTHOR
692              
693             Erik Huelsmann
694              
695             =head1 CONTRIBUTORS
696              
697             Erik Huelsmann
698             Yves Lavoie
699              
700             =head1 MAINTAINERS
701              
702             Erik Huelsmann
703              
704             =head1 BUGS AND LIMITATIONS
705              
706             Bugs can be filed in the GitHub issue tracker for the Weasel project:
707             https://github.com/perl-weasel/weasel/issues
708              
709             =head1 SOURCE
710              
711             The source code repository for Weasel is at
712             https://github.com/perl-weasel/weasel
713              
714             =head1 SUPPORT
715              
716             Community support is available through
717             L<perl-weasel@googlegroups.com|mailto:perl-weasel@googlegroups.com>.
718              
719             =head1 LICENSE AND COPYRIGHT
720              
721             (C) 2016-2023 Erik Huelsmann
722              
723             Licensed under the same terms as Perl.
724              
725             =cut
726              
727              
728             __PACKAGE__->meta->make_immutable;
729              
730             1;
731