Bug 30410: Unit tests
[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 show_all_entries {
212     my ( $self, $xpath_selector ) = @_;
213
214     $self->driver->find_element( $xpath_selector
215           . '//div[@class="dataTables_length"]/label/select/option[@value="-1"]'
216     )->click;
217     my ($all_displayed, $i);
218     my $max_retries = $self->max_retries;
219     while ( not $all_displayed ) {
220         my $dt_infos = $self->driver->get_text(
221             $xpath_selector . '//div[@class="dataTables_info"]' );
222
223         if ( $dt_infos =~ m|Showing 1 to (\d+) of (\d+) entries| ) {
224             $all_displayed = 1 if $1 == $2;
225         }
226
227         $self->driver->pause(1000) unless $all_displayed;
228
229         die "Cannot show all entries from table $xpath_selector"
230             if $max_retries <= ++$i
231     }
232 }
233
234 sub click_when_visible {
235     my ( $self, $xpath_selector ) = @_;
236
237     my $elt = $self->wait_for_element_visible( $xpath_selector );
238
239     my $clicked;
240     $self->remove_error_handler;
241     while ( not $clicked ) {
242         eval { $self->driver->find_element($xpath_selector)->click };
243         $clicked = !$@;
244         $self->driver->pause(1000) unless $clicked;
245     }
246     $self->add_error_handler;
247     $elt->click unless $clicked; # finally Raise the error
248 }
249
250 sub max_retries { 10 }
251
252 =head1 NAME
253
254 t::lib::Selenium - Selenium helper module
255
256 =head1 SYNOPSIS
257
258     my $s = t::lib::Selenium->new;
259     my $driver = $s->driver;
260     my $base_url = $s->base_url;
261     $s->auth;
262     $driver->get($s->base_url . 'mainpage.pl');
263     $s->fill_form({ input_id => 'value' });
264
265 =head1 DESCRIPTION
266
267 The goal of this module is to group the different actions we need
268 when we use automation test using Selenium
269
270 =head1 METHODS
271
272 =head2 new
273
274     my $s = t::lib::Selenium->new;
275
276     Constructor - Returns the object Selenium
277     You can pass login, password, base_url, selenium_addr, selenium_port
278     If not passed, the environment variables will be used
279     KOHA_USER, KOHA_PASS, KOHA_INTRANET_URL, SELENIUM_ADDR SELENIUM_PORT
280     Or koha, koha, syspref staffClientBaseURL, localhost, 4444
281
282 =head2 auth
283
284     $s->auth;
285
286     Will login into Koha.
287
288 =head2 fill_form
289
290     $driver->get($url)
291     $s->fill_form({
292         input_id => 'value',
293         element_id => 'other_value',
294     });
295
296     Will fill the different elements of a form.
297     The keys must be element ids (input and select are supported so far)
298     The values must a string.
299
300 =head2 submit_form
301
302     $s->submit_form;
303
304     It will submit the form using the submit button present in in the fieldset with a clas="action".
305     It should be the default way. If it does not work you should certainly fix the Koha interface.
306
307 =head2 click
308
309     $s->click
310
311     This is a bit dirty for now but will evolve depending on the needs
312     3 parameters possible but only the following 2 forms are used:
313     $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
314     $s->click({ id => 'element_id });
315
316 =head2 click_when_visible
317
318     $c->click_when_visible
319
320     Should always be called to avoid the "An element could not be located on the page" error
321
322 =head2 capture
323     $c->capture
324
325 Capture a screenshot and upload it using the excellent lut.im service provided by framasoft
326 The url of the image will be printed on STDERR (it should be better to return it instead)
327
328 =head2 add_error_handler
329     $c->add_error_handler
330
331 Add our specific error handler to the driver.
332 It will displayed a trace as well as capture a screenshot of the current screen.
333 So only case you should need it is after you called remove_error_handler
334
335 =head2 remove_error_handler
336     $c->remove_error_handler
337
338 Do *not* call this method if you are not aware of what it will do!
339 It will remove any kinds of error raised by the driver.
340 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.
341 You certainly should call it for only one statement then must call add_error_handler right after.
342
343 =head1 AUTHORS
344
345 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
346
347 Alex Buckley <alexbuckley@catalyst.net.nz>
348
349 Koha Development Team
350
351 =head1 COPYRIGHT
352
353 Copyright 2017 - Koha Development Team
354
355 =head1 LICENSE
356
357 This file is part of Koha.
358
359 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
360 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
361
362 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.
363
364 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.
365
366 =cut
367
368 1;