Bug 26363: Provide a systemd unit file for koha-worker in package installs
[koha.git] / t / lib / Selenium.pm
1 package t::lib::Selenium;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
19 use Modern::Perl;
20 use Carp qw( croak );
21 use JSON qw( from_json );
22 use File::Slurp qw( write_file );
23
24 use C4::Context;
25
26 use base qw(Class::Accessor);
27 __PACKAGE__->mk_accessors(qw(login password base_url opac_base_url selenium_addr selenium_port driver));
28
29 sub capture {
30     my ( $class, $driver ) = @_;
31
32     $driver->get_page_source;
33     write_file('/tmp/page_source_from_selenium', {binmode => ':utf8'}, $driver->get_page_source );
34     my $gdf3_url = qx(cat /tmp/page_source_from_selenium | curl --data-binary \@- https://gdf3.com);
35     print STDERR "\nPage source pasted at $gdf3_url";
36
37     my $lutim_server = q|https://pic.infini.fr/|; # Thanks Infini!
38     $driver->capture_screenshot('selenium_failure.png');
39     my $from_json = from_json qx{curl -s -F "format=json" -F "file=\@selenium_failure.png" -F "delete-day=1" $lutim_server};
40     if ( $from_json ) {
41         print STDERR "\nSCREENSHOT: $lutim_server/" . $from_json->{msg}->{short} . "\n";
42     }
43 }
44
45 sub new {
46     my ( $class, $params ) = @_;
47     my $self   = {};
48     my $config = $class->config;
49     $self->{login}    = $params->{login}    || $config->{login};
50     $self->{password} = $params->{password} || $config->{password};
51     $self->{base_url} = $params->{base_url} || $config->{base_url};
52     $self->{opac_base_url} = $params->{opac_base_url} || $config->{opac_base_url};
53     $self->{selenium_addr} = $params->{selenium_addr} || $config->{selenium_addr};
54     $self->{selenium_port} = $params->{selenium_port} || $config->{selenium_port};
55     $self->{driver} = Selenium::Remote::Driver->new(
56         port               => $self->{selenium_port},
57         remote_server_addr => $self->{selenium_addr},
58     );
59     bless $self, $class;
60     $self->add_error_handler;
61     $self->driver->set_implicit_wait_timeout(5000);
62     return $self;
63 }
64
65 sub add_error_handler {
66     my ( $self ) = @_;
67     $self->{driver}->error_handler(
68         sub {
69             my ( $driver, $selenium_error ) = @_;
70             print STDERR "\nSTRACE:";
71             my $i = 1;
72             while ( (my @call_details = (caller($i++))) ){
73                 print STDERR "\t" . $call_details[1]. ":" . $call_details[2] . " in " . $call_details[3]."\n";
74             }
75             print STDERR "\n";
76             $self->capture( $driver );
77             $driver->quit();
78             croak $selenium_error;
79         }
80     );
81 }
82
83 sub remove_error_handler {
84     my ( $self ) = @_;
85     $self->{driver}->error_handler( sub {} );
86 }
87
88 sub config {
89     return {
90         login    => $ENV{KOHA_USER} || 'koha',
91         password => $ENV{KOHA_PASS} || 'koha',
92         base_url => ( $ENV{KOHA_INTRANET_URL} || C4::Context->preference("staffClientBaseURL") ) . "/cgi-bin/koha/",
93         opac_base_url => ( $ENV{KOHA_OPAC_URL} || C4::Context->preference("OPACBaseURL") ) . "/cgi-bin/koha/",
94         selenium_addr => $ENV{SELENIUM_ADDR} || 'localhost',
95         selenium_port => $ENV{SELENIUM_PORT} || 4444,
96     };
97 }
98
99 sub auth {
100     my ( $self, $login, $password ) = @_;
101
102     $login ||= $self->login;
103     $password ||= $self->password;
104     my $mainpage = $self->base_url . 'mainpage.pl';
105
106     $self->driver->get($mainpage);
107     $self->fill_form( { userid => $login, password => $password } );
108     my $login_button = $self->driver->find_element('//input[@id="submit-button"]');
109     $login_button->click();
110 }
111
112 sub opac_auth {
113     my ( $self, $login, $password ) = @_;
114
115     $login ||= $self->login;
116     $password ||= $self->password;
117     my $mainpage = $self->opac_base_url . 'opac-main.pl';
118
119     $self->driver->get($mainpage . q|?logout.x=1|); # Logout before, to make sure we will see the login form
120     $self->driver->get($mainpage);
121     $self->fill_form( { userid => $login, password => $password } );
122     $self->submit_form;
123 }
124
125 sub fill_form {
126     my ( $self, $values ) = @_;
127     while ( my ( $id, $value ) = each %$values ) {
128         my $element = $self->driver->find_element('//*[@id="'.$id.'"]');
129         my $tag = $element->get_tag_name();
130         if ( $tag eq 'input' ) {
131             $self->driver->find_element('//input[@id="'.$id.'"]')->send_keys($value);
132         } elsif ( $tag eq 'select' ) {
133             $self->driver->find_element('//select[@id="'.$id.'"]/option[@value="'.$value.'"]')->click;
134         }
135     }
136 }
137
138 sub submit_form {
139     my ( $self ) = @_;
140
141     my $default_submit_selector = '//fieldset[@class="action"]/input[@type="submit"]';
142     $self->driver->find_element($default_submit_selector)->click
143 }
144
145 sub click {
146     my ( $self, $params ) = @_;
147     my $xpath_selector;
148     if ( exists $params->{main} ) {
149         $xpath_selector = '//div[@id="'.$params->{main}.'"]';
150     } elsif ( exists $params->{main_class} ) {
151         $xpath_selector = '//div[@class="'.$params->{main_class}.'"]';
152     }
153     if ( exists $params->{href} ) {
154         if ( ref( $params->{href} ) ) {
155             for my $k ( keys %{ $params->{href} } ) {
156                 if ( $k eq 'ends-with' ) {
157                     # ends-with version for xpath version 1
158                     my $ends_with = $params->{href}{"ends-with"};
159                     $xpath_selector .= '//a[substring(@href, string-length(@href) - string-length("'.$ends_with.'") + 1 ) = "'.$ends_with.'"]';
160                     # ends-with version for xpath version 2
161                     #$xpath_selector .= '//a[ends-with(@href, "'.$ends_with.'") ]';
162
163             } else {
164                     die "Only ends-with is supported so far ($k)";
165                 }
166             }
167         } else {
168             $xpath_selector .= '//a[contains(@href, "'.$params->{href}.'")]';
169         }
170     }
171     if ( exists $params->{id} ) {
172         $xpath_selector .= '//*[@id="'.$params->{id}.'"]';
173     }
174     $self->driver->find_element($xpath_selector)->click
175 }
176
177 sub wait_for_element_visible {
178     my ( $self, $xpath_selector ) = @_;
179
180     my ($visible, $elt);
181     $self->remove_error_handler;
182     while ( not $visible ) {
183         $elt = eval {$self->driver->find_element($xpath_selector) };
184         $visible = $elt && $elt->is_displayed;
185         $self->driver->pause(1000) unless $visible;
186     }
187     $self->add_error_handler;
188     return $elt;
189 }
190
191 sub show_all_entries {
192     my ( $self, $xpath_selector ) = @_;
193
194     $self->driver->find_element( $xpath_selector
195           . '//div[@class="dataTables_length"]/label/select/option[@value="-1"]'
196     )->click;
197     my ($all_displayed, $i);
198     my $max_retries = $self->max_retries;
199     while ( not $all_displayed ) {
200         my $dt_infos = $self->driver->get_text(
201             $xpath_selector . '//div[@class="dataTables_info"]' );
202
203         if ( $dt_infos =~ m|Showing 1 to (\d+) of (\d+) entries| ) {
204             $all_displayed = 1 if $1 == $2;
205         }
206
207         $self->driver->pause(1000) unless $all_displayed;
208
209         die "Cannot show all entries from table $xpath_selector"
210             if $max_retries <= ++$i
211     }
212 }
213
214 sub click_when_visible {
215     my ( $self, $xpath_selector ) = @_;
216
217     my $elt = $self->wait_for_element_visible( $xpath_selector );
218
219     my $clicked;
220     $self->remove_error_handler;
221     while ( not $clicked ) {
222         eval { $self->driver->find_element($xpath_selector)->click };
223         $clicked = !$@;
224         $self->driver->pause(1000) unless $clicked;
225     }
226     $self->add_error_handler;
227     $elt->click unless $clicked; # finally Raise the error
228 }
229
230 sub max_retries { 10 }
231
232 =head1 NAME
233
234 t::lib::Selenium - Selenium helper module
235
236 =head1 SYNOPSIS
237
238     my $s = t::lib::Selenium->new;
239     my $driver = $s->driver;
240     my $base_url = $s->base_url;
241     $s->auth;
242     $driver->get($s->base_url . 'mainpage.pl');
243     $s->fill_form({ input_id => 'value' });
244
245 =head1 DESCRIPTION
246
247 The goal of this module is to group the different actions we need
248 when we use automation test using Selenium
249
250 =head1 METHODS
251
252 =head2 new
253
254     my $s = t::lib::Selenium->new;
255
256     Constructor - Returns the object Selenium
257     You can pass login, password, base_url, selenium_addr, selenium_port
258     If not passed, the environment variables will be used
259     KOHA_USER, KOHA_PASS, KOHA_INTRANET_URL, SELENIUM_ADDR SELENIUM_PORT
260     Or koha, koha, syspref staffClientBaseURL, localhost, 4444
261
262 =head2 auth
263
264     $s->auth;
265
266     Will login into Koha.
267
268 =head2 fill_form
269
270     $driver->get($url)
271     $s->fill_form({
272         input_id => 'value',
273         element_id => 'other_value',
274     });
275
276     Will fill the different elements of a form.
277     The keys must be element ids (input and select are supported so far)
278     The values must a string.
279
280 =head2 submit_form
281
282     $s->submit_form;
283
284     It will submit the form using the submit button present in in the fieldset with a clas="action".
285     It should be the default way. If it does not work you should certainly fix the Koha interface.
286
287 =head2 click
288
289     $s->click
290
291     This is a bit dirty for now but will evolve depending on the needs
292     3 parameters possible but only the following 2 forms are used:
293     $s->click({ href => '/module/script.pl?foo=bar', main => 'doc3' }); # Sometimes we have doc or doc3. To make sure we are not going to hit a link in the header
294     $s->click({ id => 'element_id });
295
296 =head2 click_when_visible
297
298     $c->click_when_visible
299
300     Should always be called to avoid the "An element could not be located on the page" error
301
302 =head2 capture
303     $c->capture
304
305 Capture a screenshot and upload it using the excellent lut.im service provided by framasoft
306 The url of the image will be printed on STDERR (it should be better to return it instead)
307
308 =head2 add_error_handler
309     $c->add_error_handler
310
311 Add our specific error handler to the driver.
312 It will displayed a trace as well as capture a screenshot of the current screen.
313 So only case you should need it is after you called remove_error_handler
314
315 =head2 remove_error_handler
316     $c->remove_error_handler
317
318 Do *not* call this method if you are not aware of what it will do!
319 It will remove any kinds of error raised by the driver.
320 It can be useful in some cases, for instance if you want to make sure something will not happen and that could make the driver exploses otherwise.
321 You certainly should call it for only one statement then must call add_error_handler right after.
322
323 =head1 AUTHORS
324
325 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
326
327 Alex Buckley <alexbuckley@catalyst.net.nz>
328
329 Koha Development Team
330
331 =head1 COPYRIGHT
332
333 Copyright 2017 - Koha Development Team
334
335 =head1 LICENSE
336
337 This file is part of Koha.
338
339 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
340 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
341
342 Koha is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
343
344 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.
345
346 =cut
347
348 1;