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.16
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   35667 use strict;
  2         10  
  2         60  
48 2     2   12 use warnings;
  2         4  
  2         84  
49              
50             our $VERSION = '0.16';
51              
52              
53 2     2   10 use Digest::MD5 qw(md5_hex);
  2         5  
  2         90  
54 2     2   1107 use File::Find::Rule;
  2         16974  
  2         16  
55 2     2   968 use File::Share ':all';
  2         56166  
  2         356  
56 2     2   18 use List::Util qw(any);
  2         5  
  2         162  
57 2     2   1075 use Module::Runtime qw(use_module);
  2         3559  
  2         13  
58 2     2   1207 use Template;
  2         40250  
  2         79  
59 2     2   1217 use Test::BDD::Cucumber::Extension;
  2         218365  
  2         80  
60 2     2   1071 use YAML::Syck qw(Load);
  2         4047  
  2         144  
61              
62 2     2   1129 use Weasel;
  2         1067391  
  2         124  
63 2     2   1399 use Weasel::Session;
  2         168768  
  2         102  
64              
65 2     2   23 use Moose;
  2         6  
  2         17  
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   13 my $self = shift;
91 6         15 my ($event, $log_item, $something) = @_;
92 6 50       22 my $log_text = (ref $log_item eq 'CODE') ? $log_item->() : $log_item;
93              
94 6         635 my $log = $self->_log;
95 6 100 66     34 if ($log and not $tmp_disable_logging) {
96 2         5 push @{$log->{step}->{logs}}, {
  2         10  
97             text => $log_text
98             };
99             }
100             }
101              
102             sub _rel_url {
103 8     8   13 my $self = shift;
104              
105 8         257 my @screen_dirs = File::Spec->splitdir( File::Spec->rel2abs($self->screenshots_dir) );
106 8         250 my @logs_dirs = File::Spec->splitdir( File::Spec->rel2abs($self->logging_dir) );
107              
108 8   33     42 while (@screen_dirs and @logs_dirs) {
109 32 100       72 if ($screen_dirs[0] eq $logs_dirs[0]) {
110 24         37 shift @screen_dirs;
111 24         69 shift @logs_dirs;
112             }
113             else {
114 8         18 last;
115             }
116             }
117 8         18 my $up_dirs = '../' x (scalar(@logs_dirs));
118 8         40 return $up_dirs . join('/', @screen_dirs, '');
119             }
120              
121             sub _update_index {
122 8     8   17 my $self = shift;
123              
124 8         14 my @yaml_snippets;
125 8         62 for my $log (File::Find::Rule
126             ->name( '*.html' )
127             ->in( $self->logging_dir )) {
128 15         6207 local $/ = undef;
129 15         580 open my $fh, '<:utf8', $log;
130 15         595 my $content = <$fh>;
131 15         174 close $fh;
132              
133 15         185 my ($snippet) = ($content =~ m/<!--\n---\n(.*)---\n-->\n/s);
134 15 100       107 push @yaml_snippets, Load($snippet)
135             if $snippet;
136             }
137              
138 8         133 my $order = 'filename';
139 8         21 @yaml_snippets = sort { $a->{$order} cmp $b->{$order} } @yaml_snippets;
  0         0  
140 8         37 my $vars = {
141             features => \@yaml_snippets,
142             program => {
143             version => $VERSION,
144             },
145             };
146 8         327 my $engine = $self->_log->{template};
147 8 50       286 $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   399 my $self = shift;
157 8         234 my $log = $self->_log;
158 8 50 33     44 return if ! $log || ! $log->{feature};
159              
160 8         49 my $f = md5_hex($log->{feature}->{filename}) . '.html';
161 8         26 $log->{screens_base_url} = $self->_rel_url;
162 8         20 $log->{feature}->{log_filename} = $f;
163             $log->{template}->process(
164             $self->feature_template,
165 8         70 { %{$log} }, # using the $log object directly destroys it...
166             $f,
167             { binmode => ':utf8' })
168 8 50       284 or die $log->{template}->error();
169              
170 8         2772 $self->_update_index;
171              
172 8         3358 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     35 if ($self->screenshots_dir && !$self->logging_dir) {
179 0         0 die "Unable to generate screenshots when logging is disabled";
180             }
181 1 50       43 if ($self->logging_dir) { # the user wants logging...
182 1 50       31 die 'Logging directory: ' . $self->logging_dir . ' does not exist'
183             if ! -d $self->logging_dir;
184              
185 1         41 $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 5108 my ($self) = @_;
215              
216 1         43 my $ext_config = $self->sessions;
217 1         3 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         5 my $drv = use_module($sess->{driver}->{drv_name});
221 1         61100 $drv = $drv->new(%{$sess->{driver}});
  1         35  
222             my $session = Weasel::Session->new(
223             %$sess,
224             driver => $drv,
225 6     6   40181 log_hook => sub { $self->_weasel_log_hook(@_) },
226 1         87 );
227 1         237 $sessions{$sess_name} = $session;
228             }
229 1         37 my $weasel = Weasel->new(
230             default_session => $self->default_session,
231             sessions => \%sessions,
232             );
233 1         114 $self->_weasel($weasel);
234 1         11 $self->_initialize_logging;
235             }
236              
237             =item pre_feature
238              
239             =cut
240              
241             sub pre_feature {
242 1     1 1 11781 my ($self, $feature, $feature_stash) = @_;
243              
244 1         50 my $log = $self->_log;
245 1 50       6 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         13 map { $_->content }
254 1         20 @{$feature->satisfaction})
  1         50  
255             };
256 1         49 push @{$log->{features}}, $feature_log;
  1         4  
257 1         5 $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 7157 my ($self, $scenario, $feature_stash, $stash) = @_;
281              
282 2 50       5 if (grep { $_ eq 'weasel'} @{$scenario->tags}) {
  2         23  
  2         40  
283 2 50 33 2   9 if (any { $_ eq 'weasel-one-session' } @{$scenario->tags}
  2         46  
  2         33  
284             and $feature_stash->{ext_wsl}) {
285 0         0 $stash->{ext_wsl} = $feature_stash->{ext_wsl};
286             }
287             else {
288 2         76 $stash->{ext_wsl} = $self->_weasel->session;
289 2         184 $self->_weasel->session->start;
290             }
291 2 50   2   564 if (any { $_ eq 'weasel-one-session' } @{$scenario->tags}) {
  2         19  
  2         37  
292 0         0 $feature_stash->{ext_wsl} = $stash->{ext_wsl};
293             }
294              
295 2         62 my $log = $self->_log;
296 2 50       6 if ($log) {
297 2         38 my $scenario_log = {
298             rows => [],
299             title => $scenario->name,
300             };
301 2         17 push @{$log->{feature}->{scenarios}}, $scenario_log;
  2         7  
302 2         7 $log->{scenario} = $scenario_log;
303             }
304              
305 2         8 $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       5 return if ! defined $stash->{ext_wsl};
314 1         4 $self->_save_screenshot("scenario", "post");
315              
316 1         32 my $log = $self->_log;
317 1 50       4 if ($log) {
318 1         6 $self->_flush_log;
319 1 50       7 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         3 push @{$log->{feature}->{successes}}, $log->{scenario};
  1         5  
325             }
326 1         3 $log->{scenario} = undef;
327             }
328              
329             $stash->{ext_wsl}->stop
330 1 50   1   6 unless any { $_ eq 'weasel-one-session' } @{$scenario->tags};
  1         18  
  1         23  
331             }
332              
333             sub pre_step {
334 1     1 1 8 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         33 my $log = $self->_log;
341 1 50       5 if ($log) {
342 1         21 my $step = {
343             text => $context->step->verb_original
344             . ' ' . $context->text, # includes filled outline placeholders
345             logs => [],
346             result => '',
347             };
348 1         16 $log->{step} = $step;
349 1         3 push @{$log->{scenario}->{rows}}, { step => $step };
  1         5  
350             }
351 1         4 $self->_save_screenshot("step", "pre");
352             }
353              
354             sub post_step {
355 1     1 1 867 my ($self, $step, $context, $fail, $result) = @_;
356              
357 1 50       9 return if ! defined $context->stash->{scenario}->{ext_wsl};
358 1         40 my $log = $self->_log;
359 1         6 $self->_save_screenshot("step", "post");
360 1 50       7 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         3 $log->{step}->{result} = '<missing>'; # Pherkin <= 0.56
368             }
369 1         4 $self->_flush_log;
370 1         5 $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   43 my ($self, $event, $phase) = @_;
445              
446 5 50       173 return if ! $self->screenshots_dir;
447 5 100       208 return if ! $self->screenshot_event_on("$phase-$event");
448 2 50       75 return if $self->_weasel->session->state ne 'started';
449              
450 2         230 my $img_name = md5_hex($self->_log->{feature}->{filename}) . "-$event-$phase-" . ($img_num++) . '.png';
451 2 50       104 if (open my $fh, ">", $self->screenshots_dir . '/' . $img_name) {
452 2         9 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         83 $self->_weasel->session->screenshot($fh);
456 2 50       109 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         85 my $log = $self->_log;
464 2 50       8 if ($log) {
465 2         5 push @{$log->{scenario}->{rows}}, {
  2         28  
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             L<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-2023 Erik Huelsmann
504              
505             Licensed under the same terms as Perl.
506              
507             =cut
508              
509              
510             1;