Bug 26162: Make Selenium click action more robust
[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     return $self;
62 }
63
64 sub add_error_handler {
65     my ( $self ) = @_;
66     $self->{driver}->error_handler(
67         sub {
68             my ( $driver, $selenium_error ) = @_;
69             print STDERR "\nSTRACE:";
70             my $i = 1;
71             while ( (my @call_details = (caller($i++))) ){
72                 print STDERR "\t" . $call_details[1]. ":" . $call_details[2] . " in " . $call_details[3]."\n";
73             }
74             print STDERR "\n";
75             $self->capture( $driver );
76             croak $selenium_error;
77         }
78     );
79 }
80
81 sub remove_error_handler {
82     my ( $self ) = @_;
83     $self->{driver}->error_handler( sub {} );
84 }
85
86 sub config {
87     return {
88         login    => $ENV{KOHA_USER} || 'koha',
89         password => $ENV{KOHA_PASS} || 'koha',
90         base_url => ( $ENV{KOHA_INTRANET_URL} || C4::Context->preference("staffClientBaseURL") ) . "/cgi-bin/koha/",
91         opac_base_url => ( $ENV{KOHA_OPAC_URL} || C4::Context->preference("OPACBaseURL") ) . "/cgi-bin/koha/",
92         selenium_addr => $ENV{SELENIUM_ADDR} || 'localhost',
93         selenium_port => $ENV{SELENIUM_PORT} || 4444,
94     };
95 }
96
97 sub auth {
98     my ( $self, $login, $password ) = @_;
99
100     $login ||= $self->login;
101     $password ||= $self->password;
102     my $mainpage = $self->base_url . 'mainpage.pl';
103
104     $self->driver->get($mainpage);
105     $self->fill_form( { userid => $login, password => $password } );
106     my $login_button = $self->driver->find_element('//input[@id="submit"]');
107     $login_button->submit();
108 }
109
110 sub opac_auth {
111     my ( $self, $login, $password ) = @_;
112
113     $login ||= $self->login;
114     $password ||= $self->password;
115     my $mainpage = $self->opac_base_url . 'opac-main.pl';
116
117     $self->driver->get($mainpage . q|?logout.x=1|); # Logout before, to make sure we will see the login form
118     $self->driver->get($mainpage);
119     $self->fill_form( { userid => $login, password => $password } );
120     $self->submit_form;
121 }
122
123 sub fill_form {
124     my ( $self, $values ) = @_;
125     while ( my ( $id, $value ) = each %$values ) {
126         my $element = $self->driver->find_element('//*[@id="'.$id.'"]');
127         my $tag = $element->get_tag_name();
128         if ( $tag eq 'input' ) {
129             $self->driver->find_element('//input[@id="'.$id.'"]')->send_keys($value);
130         } elsif ( $tag eq 'select' ) {
131             $self->driver->find_element('//select[@id="'.$id.'"]/option[@value="'.$value.'"]')->click;
132         }
133     }
134 }
135
136 sub submit_form {
137     my ( $self ) = @_;
138
139     my $default_submit_selector = '//fieldset[@class="action"]/input[@type="submit"]';
140     $self->click_when_visible( $default_submit_selector );
141 }
142
143 sub click {
144     my ( $self, $params ) = @_;
145     my $xpath_selector;
146     if ( exists $params->{main} ) {
147         $xpath_selector = '//div[@id="'.$params->{main}.'"]';
148     } elsif ( exists $params->{main_class} ) {
149         $xpath_selector = '//div[@class="'.$params->{main_class}.'"]';
150     }
151     if ( exists $params->{href} ) {
152         if ( ref( $params->{href} ) ) {
153             for my $k ( keys %{ $params->{href} } ) {
154                 if ( $k eq 'ends-with' ) {
155                     # ends-with version for xpath version 1
156                     my $ends_with = $params->{href}{"ends-with"};
157                     $xpath_selector .= '//a[substring(@href, string-length(@href) - string-length("'.$ends_with.'") + 1 ) = "'.$ends_with.'"]';
158                     # ends-with version for xpath version 2
159                     #$xpath_selector .= '//a[ends-with(@href, "'.$ends_with.'") ]';
160
161             } else {
162                     die "Only ends-with is supported so far ($k)";
163                 }
164             }
165         } else {
166             $xpath_selector .= '//a[contains(@href, "'.$params->{href}.'")]';
167         }
168     }
169     if ( exists $params->{id} ) {
170         $xpath_selector .= '//*[@id="'.$params->{id}.'"]';
171     }
172     $self->click_when_visible( $xpath_selector );
173 }
174
175 sub click_when_visible {
176     my ( $self, $xpath_selector ) = @_;
177     $self->driver->set_implicit_wait_timeout(20000);
178     my ($visible, $elt);
179     while ( not $visible ) {
180         $elt = eval {$self->driver->find_element($xpath_selector) };
181         $visible = $elt && $elt->is_displayed;
182         $self->driver->pause(1000) unless $visible;
183     }
184
185     my $clicked;
186     $self->remove_error_handler;
187     while ( not $clicked ) {
188         eval { $elt->click };
189         $clicked = !$@;
190         $self->driver->pause(1000) unless $clicked;
191     }
192     $self->add_error_handler;
193     $elt->click unless $clicked; # finally Raise the error
194 }
195
196 =head1 NAME
197
198 t::lib::Selenium - Selenium helper module
199
200 =head1 SYNOPSIS
201
202     my $s = t::lib::Selenium->new;
203     my $driver = $s->driver;
204     my $base_url = $s->base_url;
205     $s->auth;
206     $driver->get($s->base_url . 'mainpage.pl');
207     $s->fill_form({ input_id => 'value' });
208
209 =head1 DESCRIPTION
210
211 The goal of this module is to group the different actions we need
212 when we use automation test using Selenium
213
214 =head1 METHODS
215
216 =head2 new
217
218     my $s = t::lib::Selenium->new;
219
220     Constructor - Returns the object Selenium
221     You can pass login, password, base_url, selenium_addr, selenium_port
222     If not passed, the environment variables will be used
223     KOHA_USER, KOHA_PASS, KOHA_INTRANET_URL, SELENIUM_ADDR SELENIUM_PORT
224     Or koha, koha, syspref staffClientBaseURL, localhost, 4444
225
226 =head2 auth
227
228     $s->auth;
229
230     Will login into Koha.
231
232 =head2 fill_form
233
234     $driver->get($url)
235     $s->fill_form({
236         input_id => 'value',
237         element_id => 'other_value',
238     });
239
240     Will fill the different elements of a form.
241     The keys must be element ids (input and select are supported so far)
242     The values must a string.
243
244 =head2 submit_form
245
246     $s->submit_form;
247
248     It will submit the form using the submit button present in in the fieldset with a clas="action".
249     It should be the default way. If it does not work you should certainly fix the Koha interface.
250
251 =head2 click
252
253     $s->click
254
255     This is a bit dirty for now but will evolve depending on the needs
256     3 parameters possible but only the following 2 forms are used:
257     $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
258     $s->click({ id => 'element_id });
259
260 =head2 click_when_visible
261
262     $c->click_when_visible
263
264     Should always be called to avoid the "An element could not be located on the page" error
265
266 =head2 capture
267     $c->capture
268
269 Capture a screenshot and upload it using the excellent lut.im service provided by framasoft
270 The url of the image will be printed on STDERR (it should be better to return it instead)
271
272 =head2 add_error_handler
273     $c->add_error_handler
274
275 Add our specific error handler to the driver.
276 It will displayed a trace as well as capture a screenshot of the current screen.
277 So only case you should need it is after you called remove_error_handler
278
279 =head2 remove_error_handler
280     $c->remove_error_handler
281
282 Do *not* call this method if you are not aware of what it will do!
283 It will remove any kinds of error raised by the driver.
284 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.
285 You certainly should call it for only one statement then must call add_error_handler right after.
286
287 =head1 AUTHORS
288
289 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
290
291 Alex Buckley <alexbuckley@catalyst.net.nz>
292
293 Koha Development Team
294
295 =head1 COPYRIGHT
296
297 Copyright 2017 - Koha Development Team
298
299 =head1 LICENSE
300
301 This file is part of Koha.
302
303 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
304 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
305
306 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.
307
308 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.
309
310 =cut
311
312 1;