1 package t::lib::Selenium;
3 # This file is part of Koha.
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.
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.
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>.
24 use base qw(Class::Accessor);
25 __PACKAGE__->mk_accessors(qw(login password base_url opac_base_url selenium_addr selenium_port driver));
28 my ( $class, $driver ) = @_;
30 $driver->capture_screenshot('selenium_failure.png');
35 my ( $class, $params ) = @_;
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},
49 $self->add_error_handler;
50 $self->driver->set_implicit_wait_timeout(10000);
54 sub add_error_handler {
56 $self->{driver}->error_handler(
58 my ( $driver, $selenium_error ) = @_;
59 print STDERR "\nSTRACE:";
61 while ( (my @call_details = (caller($i++))) ){
62 print STDERR "\t" . $call_details[1]. ":" . $call_details[2] . " in " . $call_details[3]."\n";
65 $self->capture( $driver );
67 croak $selenium_error;
72 sub remove_error_handler {
74 $self->{driver}->error_handler( sub {} );
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,
89 my ( $self, $login, $password ) = @_;
91 $login ||= $self->login;
92 $password ||= $self->password;
93 my $mainpage = $self->base_url . 'mainpage.pl';
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();
102 my ( $self, $login, $password ) = @_;
104 $login ||= $self->login;
105 $password ||= $self->password;
106 my $mainpage = $self->opac_base_url . 'opac-main.pl';
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 } );
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;
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"]');
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);
140 die "Too many forms are displayed. Cannot submit." if @elts > 1;
142 return $elts[0]->click;
146 my ( $self, $params ) = @_;
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}.'"]';
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.'") ]';
164 die "Only ends-with is supported so far ($k)";
168 $xpath_selector .= '//a[contains(@href, "'.$params->{href}.'")]';
171 if ( exists $params->{id} ) {
172 $xpath_selector .= '//*[@id="'.$params->{id}.'"]';
174 $self->driver->find_element($xpath_selector)->click
177 sub wait_for_element_visible {
178 my ( $self, $xpath_selector ) = @_;
181 $self->remove_error_handler;
182 my $max_retries = $self->max_retries;
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;
189 die "Cannot wait more for element '$xpath_selector' to be visible"
190 if $max_retries <= ++$i
192 $self->add_error_handler;
200 my $max_retries = $self->max_retries;
202 while ( not $is_ready ) {
203 $is_ready = $self->driver->execute_script('return jQuery.active == 0');
204 $self->driver->pause(1000) unless $is_ready;
206 die "Cannot wait more for jQuery to be active (wait_for_ajax)"
207 if $max_retries <= ++$i
211 sub get_next_alert_text {
215 my $max_retries = $self->max_retries;
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;
222 die "Cannot wait more for next alert (get_next_alert)"
223 if $max_retries <= ++$i;
225 $self->add_error_handler;
229 sub show_all_entries {
230 my ( $self, $xpath_selector ) = @_;
232 $self->driver->find_element( $xpath_selector
233 . '//div[@class="dataTables_length"]/label/select/option[@value="-1"]'
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"]' );
241 if ( $dt_infos =~ m|Showing 1 to (\d+) of (\d+) entries| ) {
242 $all_displayed = 1 if $1 == $2;
245 $self->driver->pause(1000) unless $all_displayed;
247 die "Cannot show all entries from table $xpath_selector"
248 if $max_retries <= ++$i
252 sub click_when_visible {
253 my ( $self, $xpath_selector ) = @_;
255 my $elt = $self->wait_for_element_visible( $xpath_selector );
258 $self->remove_error_handler;
259 while ( not $clicked ) {
260 eval { $self->driver->find_element($xpath_selector)->click };
262 $self->driver->pause(1000) unless $clicked;
264 $self->add_error_handler;
265 $elt->click unless $clicked; # finally Raise the error
268 sub max_retries { 10 }
272 t::lib::Selenium - Selenium helper module
276 my $s = t::lib::Selenium->new;
277 my $driver = $s->driver;
278 my $base_url = $s->base_url;
280 $driver->get($s->base_url . 'mainpage.pl');
281 $s->fill_form({ input_id => 'value' });
285 The goal of this module is to group the different actions we need
286 when we use automation test using Selenium
292 my $s = t::lib::Selenium->new;
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
304 Will login into Koha.
311 element_id => 'other_value',
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.
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.
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 });
334 =head2 click_when_visible
336 $c->click_when_visible
338 Should always be called to avoid the "An element could not be located on the page" error
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)
346 =head2 add_error_handler
347 $c->add_error_handler
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
353 =head2 remove_error_handler
354 $c->remove_error_handler
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.
363 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
365 Alex Buckley <alexbuckley@catalyst.net.nz>
367 Koha Development Team
371 Copyright 2017 - Koha Development Team
375 This file is part of Koha.
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.
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.
382 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.