File Coverage

blib/lib/Weasel/Session.pm
Criterion Covered Total %
statement 81 150 54.0
branch 11 28 39.2
condition 4 12 33.3
subroutine 26 55 47.2
pod 18 18 100.0
total 140 263 53.2


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