File Coverage

blib/lib/Weasel/Driver/Mock.pm
Criterion Covered Total %
statement 116 133 87.2
branch 37 52 71.1
condition 4 12 33.3
subroutine 31 34 91.1
pod 22 22 100.0
total 210 253 83.0


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             Weasel::Driver::Mock - Weasel driver for testing purposes
5              
6             =head1 VERSION
7              
8             0.01
9              
10             =head1 SYNOPSIS
11              
12             use Weasel;
13             use Weasel::Session;
14             use Weasel::Driver::Mock;
15              
16             my %opts = (
17             states => [
18             { cmd => 'get', args => [ 'http://localhost/index' ] },
19             { cmd => 'find', args => [ '//div[@id="your-id"]' ] },
20             ],
21             );
22             my $weasel = Weasel->new(
23             default_session => 'default',
24             sessions => {
25             default => Weasel::Session->new(
26             driver => Weasel::Driver::Mock->new(%opts),
27             ),
28             });
29              
30             $weasel->session->get('http://localhost/index');
31              
32              
33             =head1 DESCRIPTION
34              
35             This module implements the L<Weasel::DriverRole> protocol,
36             mimicing a true web driver session. The concept has been
37             very much inspired by DBD::Mock.
38              
39             The C<states> attribute of a Weasel::Driver::Mock object contains
40             a reference to an array of hashes. Each hash describes a C<state>.
41              
42             [ {
43             cmd => 'get', args => [ 'http://localhost/index' ]
44             },
45             {
46             cmd => 'find', args => [ '//div[@id="help-me"]' ],
47             ret => {
48             id => 'abc',
49             },
50             },
51             {
52             cmd => 'find_all', args => [ '//div' ],
53             ret_array => [
54             { id => 'abc' },
55             { id => 'def' },
56             ],
57             },
58             {
59             cmd => 'click', args => [ 'abc' ],
60             err => 'Element not visible on the page',
61             },
62             ...
63             ]
64              
65             =head2 STATES
66              
67             A state is a hash where its keys have the following meaning:
68              
69             =over
70              
71             =item cmd (required)
72              
73             The name of the function called (e.g. 'find', 'find_all' or 'get').
74              
75             =item args (optional)
76              
77             The expected list of arguments passed to the called function. When not
78             provided, the arguments of the call are not validated.
79              
80             Note that this list excludes any file handles passed in.
81              
82             =item ret (or ret_array) (optional)
83              
84             Specifies the value to be returned from the called function, or,
85             in case of C<ret_array>, the values to be returned.
86              
87             =item err (optional)
88              
89             When a state specifies an C<err> key, the called function (if it is
90             the correct one) die with the value as the argument to C<die>.
91              
92             =item content (or content_base64 or content_from_file) (optional)
93              
94             Provides the content to be written to the file handle when the called
95             function accepts a file handle argument.
96              
97             The string provided as value of C<content> will be printed to the handle.
98             The string provided as the value of C<content_base64> will be passed to
99             C<MIME::Base64::decode>. The decoded content is then written to the handle.
100             The string provided as the value of C<content_from_file> is taken as a file
101             name.
102              
103             =back
104              
105              
106             =cut
107              
108              
109             =head1 DEPENDENCIES
110              
111              
112             =cut
113              
114              
115             package Weasel::Driver::Mock;
116              
117 3     3   185873 use strict;
  3         23  
  3         87  
118 3     3   18 use warnings;
  3         6  
  3         74  
119              
120 3     3   1497 use namespace::autoclean;
  3         55224  
  3         13  
121              
122 3     3   231 use Carp;
  3         7  
  3         160  
123 3     3   1932 use Data::Compare;
  3         33000  
  3         19  
124 3     3   12984 use Data::Dumper;
  3         21190  
  3         210  
125 3     3   1480 use English qw(-no_match_vars);
  3         7144  
  3         28  
126 3     3   2753 use Time::HiRes;
  3         4208  
  3         17  
127 3     3   1760 use Weasel::DriverRole;
  3         1471412  
  3         118  
128              
129 3     3   30 use Moose;
  3         7  
  3         17  
130             with 'Weasel::DriverRole';
131              
132             our $VERSION = '0.01';
133              
134              
135             =head1 ATTRIBUTES
136              
137             =over
138              
139             =item states
140              
141             =cut
142              
143             has states => (is => 'ro', isa => 'ArrayRef', default => sub { [] });
144              
145             has _remaining_states => (is => 'rw', isa => 'ArrayRef');
146              
147             =back
148              
149             =head1 IMPLEMENTATION OF Weasel::DriverRole
150              
151             For the documentation of the methods in this section,
152             see L<Weasel::DriverRole>.
153              
154             =over
155              
156             =item implements
157              
158             =cut
159              
160             sub implements {
161 0     0 1 0 return '0.02';
162             }
163              
164              
165             =item start
166              
167             A few capabilities can be specified in t/.pherkin.yaml
168             Some can even be specified as environment variables,
169             they will be expanded here if present.
170              
171             =cut
172              
173             sub start {
174 30     30 1 136 my $self = shift;
175              
176 30         49 $self->_remaining_states([ @{$self->states} ]);
  30         786  
177              
178 30         744 return $self->started(1);
179             }
180              
181             =item stop
182              
183             =cut
184              
185             sub stop {
186 25     25 1 2535 my $self = shift;
187              
188             carp 'Web driver has states left while stop() called'
189 25 100       41 if scalar @{$self->_remaining_states};
  25         912  
190              
191 25         866 return $self->started(0);
192             }
193              
194             =item find_all
195              
196             =cut
197              
198             sub find_all {
199 1     1 1 22 my ($self, @args) = @_;
200              
201 1         5 my @rv = $self->_check_state('find_all', \@args);
202 1 50       7 return wantarray ? @rv : \@rv;
203             }
204              
205             =item get
206              
207             =cut
208              
209             sub get {
210 7     7 1 767 my ($self, @args) = @_;
211              
212 7         20 return $self->_check_state('get', \@args);
213             }
214              
215             =item wait_for
216              
217             =cut
218              
219             sub wait_for {
220 0     0 1 0 my ($self, $callback, %args) = @_;
221              
222             # Do NOT use Selenium::Waiter, it eats all exceptions!
223 0         0 my $end = time() + $args{retry_timeout};
224 0         0 my $rv;
225 0         0 do {
226 0         0 $rv = $callback->();
227 0 0       0 return $rv if $rv;
228              
229 0         0 sleep $args{poll_delay};
230             } while (time() <= $end);
231              
232 0         0 return;
233             }
234              
235              
236             =item clear
237              
238             =cut
239              
240             sub clear {
241 1     1 1 17 my ($self, @args) = @_;
242              
243 1         4 return $self->_check_state('clear', \@args);
244             }
245              
246             =item click
247              
248             =cut
249              
250             sub click {
251 2     2 1 37 my ($self, @args) = @_;
252              
253 2         10 return $self->_check_state('click', \@args);
254             }
255              
256             =item dblclick
257              
258             =cut
259              
260             sub dblclick {
261 1     1 1 20 my ($self, @args) = @_;
262              
263 1         5 return $self->_check_state('dblclick', \@args);
264             }
265              
266             =item execute_script
267              
268             =cut
269              
270             sub execute_script {
271 1     1 1 24 my ($self, @args) = @_;
272              
273 1         4 return $self->_check_state('execute_script', \@args);
274             }
275              
276             =item get_attribute($id, $att_name)
277              
278             =cut
279              
280             sub get_attribute {
281 1     1 1 18 my ($self, @args) = @_;
282              
283 1         4 return $self->_check_state('get_attribute', \@args);
284             }
285              
286             =item get_page_source($fh)
287              
288             =cut
289              
290             sub get_page_source {
291 6     6 1 399 my ($self,$fh) = @_;
292              
293 6         25 $self->_check_state('get_page_source', [], $fh);
294 4         9 return;
295             }
296              
297             =item get_text($id)
298              
299             =cut
300              
301             sub get_text {
302 1     1 1 22 my ($self, @args) = @_;
303              
304 1         4 return $self->_check_state('get_text', \@args);
305             }
306              
307             =item is_displayed($id)
308              
309             =cut
310              
311             sub is_displayed {
312 1     1 1 17 my ($self, @args) = @_;
313              
314 1         6 return $self->_check_state('is_displayed', \@args);
315             }
316              
317             =item set_attribute($id, $att_name, $value)
318              
319             =cut
320              
321             sub set_attribute {
322 1     1 1 17 my ($self, @args) = @_;
323              
324 1         5 return $self->_check_state('set_attribute', \@args);
325             }
326              
327             =item get_selected($id)
328              
329             =cut
330              
331             sub get_selected {
332 1     1 1 21 my ($self, @args) = @_;
333              
334 1         4 return $self->_check_state('get_selected', \@args);
335             }
336              
337             =item set_selected($id, $value)
338              
339             =cut
340              
341             sub set_selected {
342 1     1 1 18 my ($self, @args) = @_;
343              
344 1         5 return $self->_check_state('set_selected', \@args);
345             }
346              
347             =item screenshot($fh)
348              
349             =cut
350              
351             sub screenshot {
352 1     1 1 137 my ($self,$fh) = @_;
353              
354 1         6 $self->_check_state('screenshot', [], $fh);
355 1         3 return;
356             }
357              
358             =item send_keys($element_id, @keys)
359              
360             =cut
361              
362             sub send_keys {
363 1     1 1 22 my ($self, @args) = @_;
364              
365 1         4 return $self->_check_state('send_keys', \@args);
366             }
367              
368             =item tag_name($elem)
369              
370             =cut
371              
372             sub tag_name {
373 1     1 1 36 my ($self, @args) = @_;
374              
375 1         12 return $self->_check_state('tag_name', \@args);
376             }
377              
378             =back
379              
380             =head1 SUBROUTINES/METHODS
381              
382             This module implements the following methods in addition to the
383             Weasel::DriverRole protocol methods:
384              
385             =over
386              
387             =item set_wait_timeout
388              
389             Sets the C<wait_timeut> attribute of the object.
390              
391             =cut
392              
393             sub set_wait_timeout {
394 0     0 1 0 my ($self, @args) = @_;
395 0         0 my ($value) = @args;
396              
397 0         0 $self->_check_state('set_wait_timeout', \@args);
398 0         0 return $self->_set_wait_timeout($value);
399             }
400              
401             =item set_window_size
402              
403             Sets the C<window_size> attribute of the object.
404              
405             =cut
406              
407             sub set_window_size {
408 1     1 1 17 my ($self, @args) = @_;
409              
410 1         5 return $self->_check_state('set_window_size', \@args);
411             }
412              
413             =back
414              
415             =cut
416              
417             # PRIVATE IMPLEMENTATIONS
418              
419             my $cmp = Data::Compare->new;
420              
421             sub _copy_file {
422 1     1   4 my ($src, $tgt) = @_;
423              
424 1         3 my ($src_h, $tgt_h, $close_src, $close_tgt);
425              
426 1 50       4 if (ref $src) {
427 0         0 $src_h = $src;
428             }
429             else {
430 1 50       40 open my $sh, '<', $src
431             or croak "Can't open file $src for copying: $ERRNO";
432 1         8 $src_h = $sh;
433 1         5 $close_src = 1;
434             }
435 1         4 binmode $src_h;
436              
437 1 50       4 if (ref $tgt) {
438 1         2 $tgt_h = $tgt;
439             }
440             else {
441 0 0       0 open my $th, '<', $tgt
442             or croak "Can't open file $tgt for copying: $ERRNO";
443 0         0 $tgt_h = $th;
444 0         0 $close_tgt = 1;
445             }
446 1         8 binmode $tgt_h;
447              
448 1         7 my $buf = '';
449 1         3 my $size = 1024;
450 1         1 while (1) {
451 2         37 my ($r, $w, $t);
452 2 50       24 defined($r = sysread $src_h, $buf, $size)
453             or croak "Failed to read from source file: $ERRNO";
454 2 100       10 last unless $r;
455 1         6 for ($w = 0; $w < $r; $w += $t) {
456 1 50       8 $t = syswrite $tgt_h, $buf, $r - $w, $w
457             or croak "Failed to write to target file: $ERRNO";
458             }
459             }
460 1 50 0     7 close($tgt_h) || carp "Failed to close target file handle: $ERRNO"
461             if $close_tgt;
462 1 50 33     15 close($src_h) || carp "Failed to close source file handle: $ERRNO"
463             if $close_src;
464              
465 1         6 return 1;
466             }
467              
468              
469             sub _check_state {
470 29     29   64 my ($self, $cmd, $args, $fh) = @_;
471              
472             croak "States exhausted while '$cmd' called"
473 29 100       49 if ! @{$self->_remaining_states};
  29         855  
474              
475 28         47 my $expect = shift @{$self->_remaining_states};
  28         750  
476             croak "Mismatch between expected ($expect->{cmd}) and actual ($cmd) driver command"
477 28 100       88 if $expect->{cmd} ne $cmd;
478              
479 27 100       61 if ($expect->{args}) {
480 20 100       83 if (! $cmp->Cmp($expect->{args}, $args)) {
481             croak('Mismatch between expected and actual command arguments;'
482             . " expected:\n" . Dumper($expect->{args})
483 1         111 . "\ngot:\n" . Dumper($args))
484             }
485             }
486              
487 26 100 66     2172 if ($fh) {
    100 33        
488 6 100       48 if (defined $expect->{content}) { # empty string is false but defined
    100          
    100          
489             print ${fh} $expect->{content}
490 3 50       17 or croak "Can't write provided content to file handle for command $cmd: $ERRNO";
491             }
492             elsif ($expect->{content_from_file}) {
493 1 50       9 _copy_file $expect->{content_from_file}, $fh
494             or croak "Can't copy $expect->{content_from_file} into file handle for command $cmd: $ERRNO";
495             }
496             elsif ($expect->{content_base64}) {
497             print ${fh} MIME::Base64::decode($expect->{content_base64})
498 1 50       23 or croak "Can't write provided base64 content to file handle for command $cmd: $ERRNO";
499             }
500             else {
501 1         25 croak 'Output handle provided, but one of content/content_from_file/content_base64 missing';
502             }
503             }
504             elsif ($expect->{content} or $expect->{content_from_file}
505             or $expect->{content_base64}) {
506 1         13 croak "Content provided for command $cmd, but output handle missing";
507             }
508              
509 24 100       141 croak $expect->{err} if $expect->{err};
510              
511 23 100       47 return @{$expect->{ret_array}} if $expect->{ret_array};
  1         4  
512 22         90 return $expect->{ret};
513             }
514              
515              
516             __PACKAGE__->meta()->make_immutable();
517              
518             =head1 AUTHOR
519              
520             Erik Huelsmann
521              
522             =head1 CONTRIBUTORS
523              
524             Erik Huelsmann
525              
526             =head1 MAINTAINERS
527              
528             Erik Huelsmann
529              
530             =head1 BUGS AND LIMITATIONS
531              
532             Bugs can be filed in the GitHub issue tracker for the
533             Weasel::Driver::Mock project:
534             L<https://github.com/perl-weasel/weasel-driver-mock/issues>
535              
536             =head1 SOURCE
537              
538             The source code repository for Weasel::Driver::Mock is at
539             L<https://github.com/perl-weasel/weasel-driver-mock>
540              
541             =head1 SUPPORT
542              
543             Community support is available through
544             L<perl-weasel@googlegroups.com|mailto:perl-weasel@googlegroups.com>.
545              
546             =head1 LICENSE AND COPYRIGHT
547              
548             (C) 2019 Erik Huelsmann
549              
550             Licensed under the same terms as Perl.
551              
552             =cut
553              
554             1;
555