File Coverage

blib/lib/Pherkin/Extension/Weasel.pm
Criterion Covered Total %
statement 172 188 91.4
branch 34 64 53.1
condition 6 15 40.0
subroutine 29 31 93.5
pod 8 8 100.0
total 249 306 81.3


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             Pherkin::Extension::Weasel - Pherkin extension for web-testing
5              
6             =head1 VERSION
7              
8             0.15
9              
10             =head1 SYNOPSIS
11              
12             # In the pherkin config file t/.pherkin.yaml:
13             default:
14             extensions:
15             Pherkin::Extension::Weasel:
16             default_session: selenium
17             screenshots_dir: img
18             screenshot_events:
19             pre_step: 1
20             post_step: 1
21             post_scenario: 1
22             post_feature: 1
23             sessions:
24             selenium:
25             base_url: http://localhost:5000
26             driver:
27             drv_name: Weasel::Driver::Selenium2
28             wait_timeout: 3000
29             window_size: 1024x1280
30             caps:
31             port: 4420
32              
33             # Which makes the S->{ext_wsl} field available,
34             # pointing at the default session, in steps of features or scenarios
35             # marked with the '@weasel' tag so in the steps you can use:
36              
37             use Weasel::FindExpanders::HTML;
38              
39             Then qr/I see an input element with label XYZ/, sub {
40             S->{ext_wsl}->page->find('*labeled', text => 'XYZ');
41             };
42              
43             =cut
44              
45             package Pherkin::Extension::Weasel;
46              
47 2     2   35293 use strict;
  2         11  
  2         60  
48 2     2   11 use warnings;
  2         2  
  2         101  
49              
50             our $VERSION = '0.15';
51              
52              
53 2     2   14 use Digest::MD5 qw(md5_hex);
  2         4  
  2         98  
54 2     2   1051 use File::Find::Rule;
  2         16639  
  2         14  
55 2     2   913 use File::Share ':all';
  2         54415  
  2         356  
56 2     2   18 use List::Util qw(any);
  2         4  
  2         168  
57 2     2   1015 use Module::Runtime qw(use_module);
  2         3576  
  2         14  
58 2     2   1092 use Template;
  2         38785  
  2         66  
59 2     2   940 use Test::BDD::Cucumber::Extension;
  2         213737  
  2         81  
60 2     2   1006 use YAML::Syck qw(Load);
  2         3785  
  2         122  
61              
62 2     2   1005 use Weasel;
  2         1071494  
  2         99  
63 2     2   1307 use Weasel::Session;
  2         165937  
  2         95  
64              
65 2     2   21 use Moose;
  2         4  
  2         13  
66             extends 'Test::BDD::Cucumber::Extension';
67              
68              
69             has _log => (is => 'rw', isa => 'Maybe[HashRef]');
70              
71             has _weasel_log => (is => 'rw', isa => 'Maybe[ArrayRef]');
72              
73             has feature_template => (is => 'ro', isa => 'Str',
74             default => 'pherkin-weasel-html-log-default.html');
75             has index_template => (is => 'ro', isa => 'Str',
76             default => 'pherkin-weasel-html-log-index.html');
77              
78             has logging_dir => (is => 'ro', isa => 'Maybe[Str]');
79              
80             has templates_dir => (is => 'ro', isa => 'Str',
81             default => sub {
82             my $dist = __PACKAGE__;
83             $dist =~ s/::/-/g;
84             return dist_dir $dist;
85             });
86              
87             our $tmp_disable_logging = 0;
88              
89             sub _weasel_log_hook {
90 6     6   17 my $self = shift;
91 6         15 my ($event, $log_item, $something) = @_;
92 6 50       46 my $log_text = (ref $log_item eq 'CODE') ? $log_item->() : $log_item;
93              
94 6         674 my $log = $self->_log;
95 6 100 66     44 if ($log and not $tmp_disable_logging) {
96 2         4 push @{$log->{step}->{logs}}, {
  2         14  
97             text => $log_text
98             };
99             }
100             }
101              
102             sub _rel_url {
103 8     8   19 my $self = shift;
104              
105 8         267 my @screen_dirs = File::Spec->splitdir( File::Spec->rel2abs($self->screenshots_dir) );
106 8         282 my @logs_dirs = File::Spec->splitdir( File::Spec->rel2abs($self->logging_dir) );
107              
108 8   33     57 while (@screen_dirs and @logs_dirs) {
109 32 100       80 if ($screen_dirs[0] eq $logs_dirs[0]) {
110 24         35 shift @screen_dirs;
111 24         77 shift @logs_dirs;
112             }
113             else {
114 8         18 last;
115             }
116             }
117 8         30 my $up_dirs = '../' x (scalar(@logs_dirs));
118 8         49 return $up_dirs . join('/', @screen_dirs, '');
119             }
120              
121             sub _update_index {
122 8     8   27 my $self = shift;
123              
124 8         16 my @yaml_snippets;
125 8         78 for my $log (File::Find::Rule
126             ->name( '*.html' )
127             ->in( $self->logging_dir )) {
128 15         7383 local $/ = undef;
129 15         566 open my $fh, '<:utf8', $log;
130 15         529 my $content = <$fh>;
131 15         147 close $fh;
132              
133 15         170 my ($snippet) = ($content =~ m/<!--\n---\n(.*)---\n-->\n/s);
134 15 100       117 push @yaml_snippets, Load($snippet)
135             if $snippet;
136             }
137              
138 8         156 my $order = 'filename';
139 8         31 @yaml_snippets = sort { $a->{$order} cmp $b->{$order} } @yaml_snippets;
  0         0  
140 8         45 my $vars = {
141             features => \@yaml_snippets,
142             program => {
143             version => $VERSION,
144             },
145             };
146 8         332 my $engine = $self->_log->{template};
147 8 50       291 $engine->process(
148             $self->index_template,
149             $vars,
150             'index.html',
151             { binmode => ':utf8' })
152             or die $engine->error();
153             }
154              
155             sub _flush_log {
156 8     8   511 my $self = shift;
157 8         245 my $log = $self->_log;
158 8 50 33     64 return if ! $log || ! $log->{feature};
159              
160 8         56 my $f = md5_hex($log->{feature}->{filename}) . '.html';
161 8         35 $log->{screens_base_url} = $self->_rel_url;
162 8         22 $log->{feature}->{log_filename} = $f;
163             $log->{template}->process(
164             $self->feature_template,
165 8         79 { %{$log} }, # using the $log object directly destroys it...
166             $f,
167             { binmode => ':utf8' })
168 8 50       323 or die $log->{template}->error();
169              
170 8         3574 $self->_update_index;
171              
172 8         3454 return File::Spec->catfile($self->logging_dir, $f);
173             }
174              
175             sub _initialize_logging {
176 1     1   4 my ($self) = @_;
177              
178 1 50 33     36 if ($self->screenshots_dir && !$self->logging_dir) {
179 0         0 die "Unable to generate screenshots when logging is disabled";
180             }
181 1 50       37 if ($self->logging_dir) { # the user wants logging...
182 1 50       34 die 'Logging directory: ' . $self->logging_dir . ' does not exist'
183             if ! -d $self->logging_dir;
184              
185 1         43 $self->_log(
186             {
187             features => [],
188             template => Template->new(
189             {
190             INCLUDE_PATH => $self->templates_dir,
191             OUTPUT_PATH => $self->logging_dir,
192             }),
193             });
194             }
195             }
196              
197             =head1 Test::BDD::Cucumber::Extension protocol implementation
198              
199             =over
200              
201             =item step_directories
202              
203             =cut
204              
205             sub step_directories {
206 0     0 1 0 return [ 'weasel_steps/' ];
207             }
208              
209             =item pre_execute
210              
211             =cut
212              
213             sub pre_execute {
214 1     1 1 5459 my ($self) = @_;
215              
216 1         43 my $ext_config = $self->sessions;
217 1         2 my %sessions;
218 1         3 for my $sess_name (keys %{$ext_config}) {
  1         6  
219 1         2 my $sess = $ext_config->{$sess_name};
220 1         18 my $drv = use_module($sess->{driver}->{drv_name});
221 1         62507 $drv = $drv->new(%{$sess->{driver}});
  1         35  
222             my $session = Weasel::Session->new(
223             %$sess,
224             driver => $drv,
225 6     6   43110 log_hook => sub { $self->_weasel_log_hook(@_) },
226 1         103 );
227 1         236 $sessions{$sess_name} = $session;
228             }
229 1         36 my $weasel = Weasel->new(
230             default_session => $self->default_session,
231             sessions => \%sessions,
232             );
233 1         118 $self->_weasel($weasel);
234 1         6 $self->_initialize_logging;
235             }
236              
237             =item pre_feature
238              
239             =cut
240              
241             sub pre_feature {
242 1     1 1 12703 my ($self, $feature, $feature_stash) = @_;
243              
244 1         58 my $log = $self->_log;
245 1 50       7 if ($log) {
246             my $feature_log = {
247             scenarios => [],
248             failures => [],
249             successes => [],
250             title => $feature->name,
251             filename => $feature->document->filename,
252             satisfaction => join("\n",
253 1         15 map { $_->content }
254 1         20 @{$feature->satisfaction})
  1         53  
255             };
256 1         61 push @{$log->{features}}, $feature_log;
  1         4  
257 1         4 $log->{feature} = $feature_log;
258             }
259             }
260              
261             =item post_feature
262              
263             =cut
264              
265             sub post_feature {
266 0     0 1 0 my ($self, $feature, $feature_stash) = @_;
267              
268 0         0 my $log = $self->_log;
269 0 0       0 if ($log) {
270 0         0 $self->_flush_log;
271 0         0 $log->{feature} = undef;
272             }
273             }
274              
275             =item pre_scenario
276              
277             =cut
278              
279             sub pre_scenario {
280 2     2 1 8120 my ($self, $scenario, $feature_stash, $stash) = @_;
281              
282 2 50       5 if (grep { $_ eq 'weasel'} @{$scenario->tags}) {
  2         24  
  2         47  
283 2 50 33 2   10 if (any { $_ eq 'weasel-one-session' } @{$scenario->tags}
  2         23  
  2         32  
284             and $feature_stash->{ext_wsl}) {
285 0         0 $stash->{ext_wsl} = $feature_stash->{ext_wsl};
286             }
287             else {
288 2         74 $stash->{ext_wsl} = $self->_weasel->session;
289 2         178 $self->_weasel->session->start;
290             }
291 2 50   2   539 if (any { $_ eq 'weasel-one-session' } @{$scenario->tags}) {
  2         20  
  2         36  
292 0         0 $feature_stash->{ext_wsl} = $stash->{ext_wsl};
293             }
294              
295 2         66 my $log = $self->_log;
296 2 50       9 if ($log) {
297 2         46 my $scenario_log = {
298             rows => [],
299             title => $scenario->name,
300             };
301 2         24 push @{$log->{feature}->{scenarios}}, $scenario_log;
  2         8  
302 2         8 $log->{scenario} = $scenario_log;
303             }
304              
305 2         9 $self->_save_screenshot("scenario", "pre");
306             }
307             }
308              
309              
310             sub post_scenario {
311 1     1 1 9 my ($self, $scenario, $feature_stash, $stash) = @_;
312              
313 1 50       33 return if ! defined $stash->{ext_wsl};
314 1         6 $self->_save_screenshot("scenario", "post");
315              
316 1         33 my $log = $self->_log;
317 1 50       5 if ($log) {
318 1         6 $self->_flush_log;
319 1 50       8 if ($log->{scenario}->{failing}) {
320 0         0 push @{$log->{feature}->{failures}}, $log->{scenario};
  0         0  
321 0         0 $log->{feature}->{failing} = 1;
322             }
323             else {
324 1         2 push @{$log->{feature}->{successes}}, $log->{scenario};
  1         4  
325             }
326 1         4 $log->{scenario} = undef;
327             }
328              
329             $stash->{ext_wsl}->stop
330 1 50   1   6 unless any { $_ eq 'weasel-one-session' } @{$scenario->tags};
  1         22  
  1         26  
331             }
332              
333             sub pre_step {
334 1     1 1 11 my ($self, $step, $context) = @_;
335              
336 1 50       8 return if ! defined $context->stash->{scenario}->{ext_wsl};
337              
338             # In the logs, first announce the step, *then* show the
339             # screenshot
340 1         36 my $log = $self->_log;
341 1 50       4 if ($log) {
342 1         24 my $step = {
343             text => $context->step->verb_original
344             . ' ' . $context->text, # includes filled outline placeholders
345             logs => [],
346             result => '',
347             };
348 1         17 $log->{step} = $step;
349 1         2 push @{$log->{scenario}->{rows}}, { step => $step };
  1         5  
350             }
351 1         5 $self->_save_screenshot("step", "pre");
352             }
353              
354             sub post_step {
355 1     1 1 1100 my ($self, $step, $context, $fail, $result) = @_;
356              
357 1 50       11 return if ! defined $context->stash->{scenario}->{ext_wsl};
358 1         40 my $log = $self->_log;
359 1         7 $self->_save_screenshot("step", "post");
360 1 50       4 if ($log) {
361 1 50       4 if (ref $result) {
362 0         0 $log->{step}->{result} = $result->result;
363 0 0       0 $log->{scenario}->{failing} = 1
364             if $result->result eq 'failing';
365             }
366             else {
367 1         5 $log->{step}->{result} = '<missing>'; # Pherkin <= 0.56
368             }
369 1         4 $self->_flush_log;
370 1         7 $log->{step} = undef;
371             }
372             }
373              
374             =back
375              
376             =head1 ATTRIBUTES
377              
378             =over
379              
380             =item default_session
381              
382             =cut
383              
384             has 'default_session' => (is => 'ro');
385              
386             =item sessions
387              
388             =cut
389              
390             has 'sessions' => (is => 'ro',
391             isa => 'HashRef',
392             required => 1);
393              
394             =item base_url
395              
396             URL part to be used for prefixing URL arguments in steps
397              
398             =cut
399              
400             has base_url => ( is => 'rw', default => '' );
401              
402             =item screenshots_dir
403              
404             =cut
405              
406             has screenshots_dir => (is => 'rw', isa => 'Str');
407              
408             =item screenshot_events
409              
410             =cut
411              
412             has screenshot_events => (is => 'ro',
413             isa => 'HashRef',
414             default => sub { {} },
415             traits => ['Hash'],
416             handles => {
417             screenshot_on => 'set',
418             screenshot_off => 'delete',
419             screenshot_event_on => 'get',
420             },
421             );
422              
423             =item _weasel
424              
425             =cut
426              
427              
428             has _weasel => (is => 'rw',
429             isa => 'Weasel');
430              
431             =back
432              
433             =head1 INTERNALS
434              
435             =over
436              
437             =item _save_screenshot($event, $phase)
438              
439             =cut
440              
441             my $img_num = 0;
442              
443             sub _save_screenshot {
444 5     5   15 my ($self, $event, $phase) = @_;
445              
446 5 50       182 return if ! $self->screenshots_dir;
447 5 100       216 return if ! $self->screenshot_event_on("$phase-$event");
448 2 50       75 return if $self->_weasel->session->state ne 'started';
449              
450 2         282 my $img_name = md5_hex($self->_log->{feature}->{filename}) . "-$event-$phase-" . ($img_num++) . '.png';
451 2 50       125 if (open my $fh, ">", $self->screenshots_dir . '/' . $img_name) {
452 2         11 local $tmp_disable_logging = 1;
453             # As this is a Weasel 'command' we concoct up ourselves, don't include
454             # it in the logs for the session...
455 2         111 $self->_weasel->session->screenshot($fh);
456 2 50       111 close $fh
457             or warn "Couldn't close screenshot image '$img_name': $!";
458             }
459             else {
460 0         0 warn "Couldn't open screenshot image '$img_name': $!";
461             }
462              
463 2         116 my $log = $self->_log;
464 2 50       10 if ($log) {
465 2         3 push @{$log->{scenario}->{rows}}, {
  2         43  
466             screenshot => {
467             location => $img_name,
468             description => "$phase $event: ",
469             classes => [ $event, $phase, "$phase-$event" ],
470             },
471             };
472             }
473             }
474              
475             =back
476              
477              
478             =head1 CONTRIBUTORS
479              
480             Erik Huelsmann
481              
482             =head1 MAINTAINERS
483              
484             Erik Huelsmann
485              
486             =head1 BUGS
487              
488             Bugs can be filed in the GitHub issue tracker for the Weasel project:
489             https://github.com/perl-weasel/weasel-driver-selenium2/issues
490              
491             =head1 SOURCE
492              
493             The source code repository for Weasel is at
494             https://github.com/perl-weasel/weasel-driver-selenium2
495              
496             =head1 SUPPORT
497              
498             Community support is available through
499             L<perl-weasel@googlegroups.com|mailto:perl-weasel@googlegroups.com>.
500              
501             =head1 COPYRIGHT
502              
503             (C) 2016-2020 Erik Huelsmann
504              
505             Licensed under the same terms as Perl.
506              
507             =cut
508              
509              
510             1;