Bug 14251: Allow use of CSS in discharge letter
[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
22 use C4::Context;
23
24 use base qw(Class::Accessor);
25 __PACKAGE__->mk_accessors(qw(login password base_url opac_base_url selenium_addr selenium_port driver));
26
27 sub capture {
28     my ( $class, $driver ) = @_;
29
30     $driver->capture_screenshot('selenium_failure.png');
31
32 }
33
34 sub new {
35     my ( $class, $params ) = @_;
36     my $self   = {};
37     my $config = $class->config;
38     $self->{login}    = $params->{login}    || $config->{login};
39     $self->{password} = $params->{password} || $config->{password};
40     $self->{base_url} = $params->{base_url} || $config->{base_url};
41     $self->{opac_base_url} = $params->{opac_base_url} || $config->{opac_base_url};
42     $self->{selenium_addr} = $params->{selenium_addr} || $config->{selenium_addr};
43     $self->{selenium_port} = $params->{selenium_port} || $config->{selenium_port};
44     $self->{driver} = Selenium::Remote::Driver->new(
45         port               => $self->{selenium_port},
46         remote_server_addr => $self->{selenium_addr},
47     );
48     bless $self, $class;
49     $self->add_error_handler;
50     $self->driver->set_implicit_wait_timeout(10000);
51     return $self;
52 }
53
54 sub add_error_handler {
55     my ( $self ) = @_;
56     $self->{driver}->error_handler(
57         sub {
58             my ( $driver, $selenium_error ) = @_;
59             print STDERR "\nSTRACE:";
60             my $i = 1;
61             while ( (my @call_details = (caller($i++))) ){
62                 print STDERR "\t" . $call_details[1]. ":" . $call_details[2] . " in " . $call_details[3]."\n";
63             }
64             print STDERR "\n";
65             $self->capture( $driver );
66             $driver->quit();
67             croak $selenium_error;
68         }
69     );
70 }
71
72 sub remove_error_handler {
73     my ( $self ) = @_;
74     $self->{driver}->error_handler( sub {} );
75 }
76
77 sub config {
78     return {
79         login    => $ENV{KOHA_USER} || 'koha',
80         password => $ENV{KOHA_PASS} || 'koha',
81         base_url => ( $ENV{KOHA_INTRANET_URL} || C4::Context->preference("staffClientBaseURL") ) . "/cgi-bin/koha/",
82         opac_base_url => ( $ENV{KOHA_OPAC_URL} || C4::Context->preference("OPACBaseURL") ) . "/cgi-bin/koha/",
83         selenium_addr => $ENV{SELENIUM_ADDR} || 'localhost',
84         selenium_port => $ENV{SELENIUM_PORT} || 4444,
85     };
86 }
87
88 sub auth {
89     my ( $self, $login, $password ) = @_;
90
91     $login ||= $self->login;
92     $password ||= $self->password;
93     my $mainpage = $self->base_url . 'mainpage.pl';
94
95     $self->driver->get($mainpage);
96     $self->fill_form( { userid => $login, password => $password } );
97     my $login_button = $self->driver->find_element('//input[@id="submit-button"]');
98     $login_button->click();
99 }
100
101 sub opac_auth {
102     my ( $self, $login, $password ) = @_;
103
104     $login ||= $self->login;
105     $password ||= $self->password;
106     my $mainpage = $self->opac_base_url . 'opac-main.pl';
107
108     $self->driver->get($mainpage . q|?logout.x=1|); # Logout before, to make sure we will see the login form
109     $self->driver->get($mainpage);
110     $self->fill_form( { userid => $login, password => $password } );
111     $self->submit_form;
112 }
113
114 sub fill_form {
115     my ( $self, $values ) = @_;
116     while ( my ( $id, $value ) = each %$values ) {
117         my $element = $self->driver->find_element('//*[@id="'.$id.'"]');
118         my $tag = $element->get_tag_name();
119         if ( $tag eq 'input' ) {
120             $self->driver->find_element('//input[@id="'.$id.'"]')->send_keys($value);
121         } elsif ( $tag eq 'select' ) {
122             $self->driver->find_element('//select[@id="'.$id.'"]//option[@value="'.$value.'"]')->click;
123         }
124     }
125 }
126
127 sub submit_form {
128     my ( $self ) = @_;
129
130     # If there is only one submit element on the page we use it
131     my @submit_elements = $self->driver->find_elements('//input[@type="submit"]');
132     if ( @submit_elements == 1 ) {
133         $self->click_when_visible('//input[@type="submit"]');
134         return;
135     }
136
137     my $default_submit_selector = '//fieldset[@class="action"]/input[@type="submit"]';
138     my @elts = map { my $size = $_->get_size; ( $size->{height} && $size->{width} ) ? $_ : () } $self->driver->find_elements($default_submit_selector);
139
140     die "Too many forms are displayed. Cannot submit." if @elts > 1;
141
142     return $elts[0]->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     my $max_retries = $self->max_retries;
183     my $i;
184     while ( not $visible ) {
185         $elt = eval {$self->driver->find_element($xpath_selector) };
186         $visible = $elt && $elt->is_displayed;
187         $self->driver->pause(1000) unless $visible;
188
189         die "Cannot wait more for element '$xpath_selector' to be visible"
190             if $max_retries <= ++$i
191     }
192     $self->add_error_handler;
193     return $elt;
194 }
195
196 sub wait_for_ajax {
197     my ( $self ) = @_;
198
199     my $is_ready;
200     my $max_retries = $self->max_retries;
201     my $i;
202     while ( not $is_ready ) {
203         $is_ready = $self->driver->execute_script('return jQuery.active == 0');
204         $self->driver->pause(1000) unless $is_ready;
205
206         die "Cannot wait more for jQuery to be active (wait_for_ajax)"
207             if $max_retries <= ++$i
208     }
209 }
210
211 sub get_next_alert_text {
212     my ( $self ) = @_;
213
214     my $alert_text;
215     my $max_retries = $self->max_retries;
216     my $i;
217     $self->remove_error_handler;
218     while ( not $alert_text ) {
219         $alert_text = eval { $self->driver->get_alert_text };
220         $self->driver->pause(1000) unless $alert_text;
221
222         die "Cannot wait more for next alert (get_next_alert)"
223           if $max_retries <= ++$i;
224     }
225     $self->add_error_handler;
226     return $alert_text;
227 }
228
229 sub show_all_entries {
230     my ( $self, $xpath_selector ) = @_;
231
232     $self->driver->find_element( $xpath_selector
233           . '//div[@class="dataTables_length"]/label/select/option[@value="-1"]'
234     )->click;
235     my ($all_displayed, $i);
236     my $max_retries = $self->max_retries;
237     while ( not $all_displayed ) {
238         my $dt_infos = $self->driver->get_text(
239             $xpath_selector . '//div[@class="dataTables_info"]' );
240
241         if ( $dt_infos =~ m|Showing 1 to (\d+) of (\d+) entries| ) {
242             $all_displayed = 1 if $1 == $2;
243         }
244
245         $self->driver->pause(1000) unless $all_displayed;
246
247         die "Cannot show all entries from table $xpath_selector"
248             if $max_retries <= ++$i
249     }
250 }
251
252 sub click_when_visible {
253     my ( $self, $xpath_selector ) = @_;
254
255     my $elt = $self->wait_for_element_visible( $xpath_selector );
256
257     my $clicked;
258     $self->remove_error_handler;
259     while ( not $clicked ) {
260         eval { $self->driver->find_element($xpath_selector)->click };
261         $clicked = !$@;
262         $self->driver->pause(1000) unless $clicked;
263     }
264     $self->add_error_handler;
265     $elt->click unless $clicked; # finally Raise the error
266 }
267
268 sub max_retries { 10 }
269
270 =head1 NAME
271
272 t::lib::Selenium - Selenium helper module
273
274 =head1 SYNOPSIS
275
276     my $s = t::lib::Selenium->new;
277     my $driver = $s->driver;
278     my $base_url = $s->base_url;
279     $s->auth;
280     $driver->get($s->base_url . 'mainpage.pl');
281     $s->fill_form({ input_id => 'value' });
282
283 =head1 DESCRIPTION
284
285 The goal of this module is to group the different actions we need
286 when we use automation test using Selenium
287
288 =head1 METHODS
289
290 =head2 new
291
292     my $s = t::lib::Selenium->new;
293
294     Constructor - Returns the object Selenium
295     You can pass login, password, base_url, selenium_addr, selenium_port
296     If not passed, the environment variables will be used
297     KOHA_USER, KOHA_PASS, KOHA_INTRANET_URL, SELENIUM_ADDR SELENIUM_PORT
298     Or koha, koha, syspref staffClientBaseURL, localhost, 4444
299
300 =head2 auth
301
302     $s->auth;
303
304     Will login into Koha.
305
306 =head2 fill_form
307
308     $driver->get($url)
309     $s->fill_form({
310         input_id => 'value',
311         element_id => 'other_value',
312     });
313
314     Will fill the different elements of a form.
315     The keys must be element ids (input and select are supported so far)
316     The values must a string.
317
318 =head2 submit_form
319
320     $s->submit_form;
321
322     It will submit the form using the submit button present in in the fieldset with a clas="action".
323     It should be the default way. If it does not work you should certainly fix the Koha interface.
324
325 =head2 click
326
327     $s->click
328
329     This is a bit dirty for now but will evolve depending on the needs
330     3 parameters possible but only the following 2 forms are used:
331     $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
332     $s->click({ id => 'element_id });
333
334 =head2 click_when_visible
335
336     $c->click_when_visible
337
338     Should always be called to avoid the "An element could not be located on the page" error
339
340 =head2 capture
341     $c->capture
342
343 Capture a screenshot and upload it using the excellent lut.im service provided by framasoft
344 The url of the image will be printed on STDERR (it should be better to return it instead)
345
346 =head2 add_error_handler
347     $c->add_error_handler
348
349 Add our specific error handler to the driver.
350 It will displayed a trace as well as capture a screenshot of the current screen.
351 So only case you should need it is after you called remove_error_handler
352
353 =head2 remove_error_handler
354     $c->remove_error_handler
355
356 Do *not* call this method if you are not aware of what it will do!
357 It will remove any kinds of error raised by the driver.
358 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.
359 You certainly should call it for only one statement then must call add_error_handler right after.
360
361 =head1 AUTHORS
362
363 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
364
365 Alex Buckley <alexbuckley@catalyst.net.nz>
366
367 Koha Development Team
368
369 =head1 COPYRIGHT
370
371 Copyright 2017 - Koha Development Team
372
373 =head1 LICENSE
374
375 This file is part of Koha.
376
377 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
378 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
379
380 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.
381
382 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.
383
384 =cut
385
386 1;