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>.
21 use JSON qw( from_json );
22 use File::Slurp qw( write_file );
26 use base qw(Class::Accessor);
27 __PACKAGE__->mk_accessors(qw(login password base_url opac_base_url selenium_addr selenium_port driver));
30 my ( $class, $driver ) = @_;
32 $driver->capture_screenshot('selenium_failure.png');
37 my ( $class, $params ) = @_;
39 my $config = $class->config;
40 $self->{login} = $params->{login} || $config->{login};
41 $self->{password} = $params->{password} || $config->{password};
42 $self->{base_url} = $params->{base_url} || $config->{base_url};
43 $self->{opac_base_url} = $params->{opac_base_url} || $config->{opac_base_url};
44 $self->{selenium_addr} = $params->{selenium_addr} || $config->{selenium_addr};
45 $self->{selenium_port} = $params->{selenium_port} || $config->{selenium_port};
46 $self->{driver} = Selenium::Remote::Driver->new(
47 port => $self->{selenium_port},
48 remote_server_addr => $self->{selenium_addr},
51 $self->add_error_handler;
52 $self->driver->set_implicit_wait_timeout(5000);
56 sub add_error_handler {
58 $self->{driver}->error_handler(
60 my ( $driver, $selenium_error ) = @_;
61 print STDERR "\nSTRACE:";
63 while ( (my @call_details = (caller($i++))) ){
64 print STDERR "\t" . $call_details[1]. ":" . $call_details[2] . " in " . $call_details[3]."\n";
67 $self->capture( $driver );
69 croak $selenium_error;
74 sub remove_error_handler {
76 $self->{driver}->error_handler( sub {} );
81 login => $ENV{KOHA_USER} || 'koha',
82 password => $ENV{KOHA_PASS} || 'koha',
83 base_url => ( $ENV{KOHA_INTRANET_URL} || C4::Context->preference("staffClientBaseURL") ) . "/cgi-bin/koha/",
84 opac_base_url => ( $ENV{KOHA_OPAC_URL} || C4::Context->preference("OPACBaseURL") ) . "/cgi-bin/koha/",
85 selenium_addr => $ENV{SELENIUM_ADDR} || 'localhost',
86 selenium_port => $ENV{SELENIUM_PORT} || 4444,
91 my ( $self, $login, $password ) = @_;
93 $login ||= $self->login;
94 $password ||= $self->password;
95 my $mainpage = $self->base_url . 'mainpage.pl';
97 $self->driver->get($mainpage);
98 $self->fill_form( { userid => $login, password => $password } );
99 my $login_button = $self->driver->find_element('//input[@id="submit-button"]');
100 $login_button->click();
104 my ( $self, $login, $password ) = @_;
106 $login ||= $self->login;
107 $password ||= $self->password;
108 my $mainpage = $self->opac_base_url . 'opac-main.pl';
110 $self->driver->get($mainpage . q|?logout.x=1|); # Logout before, to make sure we will see the login form
111 $self->driver->get($mainpage);
112 $self->fill_form( { userid => $login, password => $password } );
117 my ( $self, $values ) = @_;
118 while ( my ( $id, $value ) = each %$values ) {
119 my $element = $self->driver->find_element('//*[@id="'.$id.'"]');
120 my $tag = $element->get_tag_name();
121 if ( $tag eq 'input' ) {
122 $self->driver->find_element('//input[@id="'.$id.'"]')->send_keys($value);
123 } elsif ( $tag eq 'select' ) {
124 $self->driver->find_element('//select[@id="'.$id.'"]/option[@value="'.$value.'"]')->click;
132 my $default_submit_selector = '//fieldset[@class="action"]/input[@type="submit"]';
133 $self->driver->find_element($default_submit_selector)->click
137 my ( $self, $params ) = @_;
139 if ( exists $params->{main} ) {
140 $xpath_selector = '//div[@id="'.$params->{main}.'"]';
141 } elsif ( exists $params->{main_class} ) {
142 $xpath_selector = '//div[@class="'.$params->{main_class}.'"]';
144 if ( exists $params->{href} ) {
145 if ( ref( $params->{href} ) ) {
146 for my $k ( keys %{ $params->{href} } ) {
147 if ( $k eq 'ends-with' ) {
148 # ends-with version for xpath version 1
149 my $ends_with = $params->{href}{"ends-with"};
150 $xpath_selector .= '//a[substring(@href, string-length(@href) - string-length("'.$ends_with.'") + 1 ) = "'.$ends_with.'"]';
151 # ends-with version for xpath version 2
152 #$xpath_selector .= '//a[ends-with(@href, "'.$ends_with.'") ]';
155 die "Only ends-with is supported so far ($k)";
159 $xpath_selector .= '//a[contains(@href, "'.$params->{href}.'")]';
162 if ( exists $params->{id} ) {
163 $xpath_selector .= '//*[@id="'.$params->{id}.'"]';
165 $self->driver->find_element($xpath_selector)->click
168 sub wait_for_element_visible {
169 my ( $self, $xpath_selector ) = @_;
172 $self->remove_error_handler;
173 my $max_retries = $self->max_retries;
175 while ( not $visible ) {
176 $elt = eval {$self->driver->find_element($xpath_selector) };
177 $visible = $elt && $elt->is_displayed;
178 $self->driver->pause(1000) unless $visible;
180 die "Cannot wait more for element '$xpath_selector' to be visible"
181 if $max_retries <= ++$i
183 $self->add_error_handler;
187 sub show_all_entries {
188 my ( $self, $xpath_selector ) = @_;
190 $self->driver->find_element( $xpath_selector
191 . '//div[@class="dataTables_length"]/label/select/option[@value="-1"]'
193 my ($all_displayed, $i);
194 my $max_retries = $self->max_retries;
195 while ( not $all_displayed ) {
196 my $dt_infos = $self->driver->get_text(
197 $xpath_selector . '//div[@class="dataTables_info"]' );
199 if ( $dt_infos =~ m|Showing 1 to (\d+) of (\d+) entries| ) {
200 $all_displayed = 1 if $1 == $2;
203 $self->driver->pause(1000) unless $all_displayed;
205 die "Cannot show all entries from table $xpath_selector"
206 if $max_retries <= ++$i
210 sub click_when_visible {
211 my ( $self, $xpath_selector ) = @_;
213 my $elt = $self->wait_for_element_visible( $xpath_selector );
216 $self->remove_error_handler;
217 while ( not $clicked ) {
218 eval { $self->driver->find_element($xpath_selector)->click };
220 $self->driver->pause(1000) unless $clicked;
222 $self->add_error_handler;
223 $elt->click unless $clicked; # finally Raise the error
226 sub max_retries { 10 }
230 t::lib::Selenium - Selenium helper module
234 my $s = t::lib::Selenium->new;
235 my $driver = $s->driver;
236 my $base_url = $s->base_url;
238 $driver->get($s->base_url . 'mainpage.pl');
239 $s->fill_form({ input_id => 'value' });
243 The goal of this module is to group the different actions we need
244 when we use automation test using Selenium
250 my $s = t::lib::Selenium->new;
252 Constructor - Returns the object Selenium
253 You can pass login, password, base_url, selenium_addr, selenium_port
254 If not passed, the environment variables will be used
255 KOHA_USER, KOHA_PASS, KOHA_INTRANET_URL, SELENIUM_ADDR SELENIUM_PORT
256 Or koha, koha, syspref staffClientBaseURL, localhost, 4444
262 Will login into Koha.
269 element_id => 'other_value',
272 Will fill the different elements of a form.
273 The keys must be element ids (input and select are supported so far)
274 The values must a string.
280 It will submit the form using the submit button present in in the fieldset with a clas="action".
281 It should be the default way. If it does not work you should certainly fix the Koha interface.
287 This is a bit dirty for now but will evolve depending on the needs
288 3 parameters possible but only the following 2 forms are used:
289 $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
290 $s->click({ id => 'element_id });
292 =head2 click_when_visible
294 $c->click_when_visible
296 Should always be called to avoid the "An element could not be located on the page" error
301 Capture a screenshot and upload it using the excellent lut.im service provided by framasoft
302 The url of the image will be printed on STDERR (it should be better to return it instead)
304 =head2 add_error_handler
305 $c->add_error_handler
307 Add our specific error handler to the driver.
308 It will displayed a trace as well as capture a screenshot of the current screen.
309 So only case you should need it is after you called remove_error_handler
311 =head2 remove_error_handler
312 $c->remove_error_handler
314 Do *not* call this method if you are not aware of what it will do!
315 It will remove any kinds of error raised by the driver.
316 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.
317 You certainly should call it for only one statement then must call add_error_handler right after.
321 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
323 Alex Buckley <alexbuckley@catalyst.net.nz>
325 Koha Development Team
329 Copyright 2017 - Koha Development Team
333 This file is part of Koha.
335 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
336 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
338 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.
340 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.