File Coverage

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