| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Selenium::Client; |
|
2
|
|
|
|
|
|
|
$Selenium::Client::VERSION = '2.01'; |
|
3
|
|
|
|
|
|
|
# ABSTRACT: Module for communicating with WC3 standard selenium servers |
|
4
|
|
|
|
|
|
|
|
|
5
|
2
|
|
|
2
|
|
281394
|
use strict; |
|
|
2
|
|
|
|
|
5
|
|
|
|
2
|
|
|
|
|
118
|
|
|
6
|
2
|
|
|
2
|
|
14
|
use warnings; |
|
|
2
|
|
|
|
|
5
|
|
|
|
2
|
|
|
|
|
182
|
|
|
7
|
|
|
|
|
|
|
|
|
8
|
2
|
|
|
2
|
|
42
|
use 5.006; |
|
|
2
|
|
|
|
|
14
|
|
|
9
|
2
|
|
|
2
|
|
22
|
use v5.28.0; # Before 5.006, v5.10.0 would not be understood. |
|
|
2
|
|
|
|
|
6
|
|
|
10
|
|
|
|
|
|
|
|
|
11
|
2
|
|
|
2
|
|
12
|
no warnings 'experimental'; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
178
|
|
|
12
|
2
|
|
|
2
|
|
16
|
use feature qw/signatures/; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
344
|
|
|
13
|
|
|
|
|
|
|
|
|
14
|
2
|
|
|
2
|
|
1332
|
use JSON::MaybeXS(); |
|
|
2
|
|
|
|
|
17079
|
|
|
|
2
|
|
|
|
|
68
|
|
|
15
|
2
|
|
|
2
|
|
1849
|
use HTTP::Tiny(); |
|
|
2
|
|
|
|
|
137952
|
|
|
|
2
|
|
|
|
|
128
|
|
|
16
|
2
|
|
|
2
|
|
24
|
use Carp qw{confess cluck}; |
|
|
2
|
|
|
|
|
5
|
|
|
|
2
|
|
|
|
|
154
|
|
|
17
|
2
|
|
|
2
|
|
13
|
use File::Path qw{make_path}; |
|
|
2
|
|
|
|
|
6
|
|
|
|
2
|
|
|
|
|
176
|
|
|
18
|
2
|
|
|
2
|
|
1491
|
use File::HomeDir(); |
|
|
2
|
|
|
|
|
16997
|
|
|
|
2
|
|
|
|
|
108
|
|
|
19
|
2
|
|
|
2
|
|
1356
|
use File::Slurper(); |
|
|
2
|
|
|
|
|
9836
|
|
|
|
2
|
|
|
|
|
64
|
|
|
20
|
2
|
|
|
2
|
|
17
|
use File::Spec(); |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
42
|
|
|
21
|
2
|
|
|
2
|
|
706
|
use Sub::Install(); |
|
|
2
|
|
|
|
|
2655
|
|
|
|
2
|
|
|
|
|
48
|
|
|
22
|
2
|
|
|
2
|
|
1651
|
use Net::EmptyPort(); |
|
|
2
|
|
|
|
|
9003
|
|
|
|
2
|
|
|
|
|
75
|
|
|
23
|
2
|
|
|
2
|
|
2581
|
use Capture::Tiny qw{capture_merged}; |
|
|
2
|
|
|
|
|
46376
|
|
|
|
2
|
|
|
|
|
179
|
|
|
24
|
2
|
|
|
2
|
|
1736
|
use Unicode::Normalize qw{NFC}; |
|
|
2
|
|
|
|
|
8661
|
|
|
|
2
|
|
|
|
|
223
|
|
|
25
|
|
|
|
|
|
|
|
|
26
|
2
|
|
|
2
|
|
1519
|
use Selenium::Specification; |
|
|
2
|
|
|
|
|
15
|
|
|
|
2
|
|
|
|
|
11003
|
|
|
27
|
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
|
|
29
|
0
|
|
|
0
|
1
|
|
sub new ( $class, %options ) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
30
|
0
|
|
0
|
|
|
|
$options{version} //= 'stable'; |
|
31
|
0
|
|
0
|
|
|
|
$options{port} //= 4444; |
|
32
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
#XXX geckodriver doesn't bind to localhost lol |
|
34
|
0
|
|
0
|
|
|
|
$options{host} //= '127.0.0.1'; |
|
35
|
0
|
0
|
|
|
|
|
$options{host} = '127.0.0.1' if $options{host} eq 'localhost'; |
|
36
|
|
|
|
|
|
|
|
|
37
|
0
|
|
0
|
|
|
|
$options{nofetch} //= 1; |
|
38
|
0
|
|
0
|
|
|
|
$options{scheme} //= 'http'; |
|
39
|
0
|
|
0
|
|
|
|
$options{prefix} //= ''; |
|
40
|
0
|
|
0
|
|
|
|
$options{ua} //= HTTP::Tiny->new(); |
|
41
|
0
|
|
0
|
|
|
|
$options{client_dir} //= File::HomeDir::my_home() . "/.selenium"; |
|
42
|
0
|
|
0
|
|
|
|
$options{driver} //= "SeleniumHQ::Jar"; |
|
43
|
0
|
|
0
|
|
|
|
$options{post_callbacks} //= []; |
|
44
|
0
|
|
0
|
|
|
|
$options{auto_close} //= 1; |
|
45
|
0
|
|
0
|
|
|
|
$options{browser} //= ''; |
|
46
|
0
|
|
0
|
|
|
|
$options{headless} //= 1; |
|
47
|
0
|
|
0
|
|
|
|
$options{normalize} //= 1; |
|
48
|
0
|
|
0
|
|
|
|
$options{fatal} //= 1; |
|
49
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
# Use the hardcoded JSON version of the stable spec in Selenium::Specification's DATA section. |
|
51
|
0
|
|
0
|
|
|
|
$options{hardcode} //= 1; |
|
52
|
0
|
|
0
|
|
|
|
$options{capabilities} //= {}; |
|
53
|
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
#create client_dir and log-dir |
|
55
|
0
|
|
|
|
|
|
my $dir = File::Spec->catdir( $options{client_dir}, "perl-client" ); |
|
56
|
0
|
|
|
|
|
|
make_path($dir); |
|
57
|
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
#Grab the spec |
|
59
|
0
|
|
|
|
|
|
$options{spec} = Selenium::Specification::read( $options{client_dir}, $options{version}, $options{nofetch}, $options{hardcode} ); |
|
60
|
|
|
|
|
|
|
|
|
61
|
0
|
|
|
|
|
|
my $self = bless( \%options, $class ); |
|
62
|
0
|
|
|
|
|
|
$self->{sessions} = []; |
|
63
|
|
|
|
|
|
|
|
|
64
|
0
|
|
|
|
|
|
$self->_build_subs(); |
|
65
|
0
|
0
|
|
|
|
|
$self->_spawn() if $options{host} eq '127.0.0.1'; |
|
66
|
0
|
|
|
|
|
|
return $self; |
|
67
|
|
|
|
|
|
|
} |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
|
|
70
|
0
|
|
|
0
|
1
|
|
sub catalog ( $self, $printed = 0 ) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
71
|
0
|
0
|
|
|
|
|
return $self->{spec} unless $printed; |
|
72
|
0
|
|
|
|
|
|
foreach my $method ( keys( %{ $self->{spec} } ) ) { |
|
|
0
|
|
|
|
|
|
|
|
73
|
0
|
|
|
|
|
|
print "$method: $self->{spec}{$method}{href}\n"; |
|
74
|
|
|
|
|
|
|
} |
|
75
|
0
|
|
|
|
|
|
return $self->{spec}; |
|
76
|
|
|
|
|
|
|
} |
|
77
|
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
my %browser_opts = ( |
|
79
|
|
|
|
|
|
|
firefox => { |
|
80
|
|
|
|
|
|
|
name => 'moz:firefoxOptions', |
|
81
|
|
|
|
|
|
|
headless => sub ($c) { |
|
82
|
|
|
|
|
|
|
$c->{args} //= []; |
|
83
|
|
|
|
|
|
|
push( @{ $c->{args} }, '-headless' ); |
|
84
|
|
|
|
|
|
|
}, |
|
85
|
|
|
|
|
|
|
}, |
|
86
|
|
|
|
|
|
|
chrome => { |
|
87
|
|
|
|
|
|
|
name => 'goog:chromeOptions', |
|
88
|
|
|
|
|
|
|
headless => sub ($c) { |
|
89
|
|
|
|
|
|
|
$c->{args} //= []; |
|
90
|
|
|
|
|
|
|
push( @{ $c->{args} }, 'headless' ); |
|
91
|
|
|
|
|
|
|
}, |
|
92
|
|
|
|
|
|
|
}, |
|
93
|
|
|
|
|
|
|
MicrosoftEdge => { |
|
94
|
|
|
|
|
|
|
name => 'ms:EdgeOptions', |
|
95
|
|
|
|
|
|
|
headless => sub ($c) { |
|
96
|
|
|
|
|
|
|
$c->{args} //= []; |
|
97
|
|
|
|
|
|
|
push( @{ $c->{args} }, 'headless' ); |
|
98
|
|
|
|
|
|
|
}, |
|
99
|
|
|
|
|
|
|
}, |
|
100
|
|
|
|
|
|
|
); |
|
101
|
|
|
|
|
|
|
|
|
102
|
0
|
|
|
0
|
|
|
sub _build_caps ( $self, %options ) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
103
|
0
|
0
|
|
|
|
|
$options{browser} = $self->{browser} if $self->{browser}; |
|
104
|
0
|
0
|
|
|
|
|
$options{headless} = $self->{headless} if $self->{headless}; |
|
105
|
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
my $c = { |
|
107
|
|
|
|
|
|
|
browserName => $options{browser}, |
|
108
|
0
|
|
|
|
|
|
%{ $self->{capabilities} }, |
|
|
0
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
}; |
|
110
|
0
|
|
|
|
|
|
my $browser = $browser_opts{ $options{browser} }; |
|
111
|
|
|
|
|
|
|
|
|
112
|
0
|
0
|
|
|
|
|
if ($browser) { |
|
113
|
0
|
|
|
|
|
|
my $browseropts = {}; |
|
114
|
0
|
|
|
|
|
|
foreach my $k ( keys %$browser ) { |
|
115
|
0
|
0
|
|
|
|
|
next if $k eq 'name'; |
|
116
|
0
|
0
|
|
|
|
|
$browser->{$k}->($browseropts) if $options{$k}; |
|
117
|
|
|
|
|
|
|
} |
|
118
|
0
|
|
|
|
|
|
$c->{ $browser->{name} } = $browseropts; |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
return ( |
|
122
|
0
|
|
|
|
|
|
capabilities => { |
|
123
|
|
|
|
|
|
|
alwaysMatch => $c, |
|
124
|
|
|
|
|
|
|
}, |
|
125
|
|
|
|
|
|
|
); |
|
126
|
|
|
|
|
|
|
} |
|
127
|
|
|
|
|
|
|
|
|
128
|
0
|
|
|
0
|
|
|
sub _build_subs ($self) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
129
|
0
|
|
|
|
|
|
foreach my $sub ( keys( %{ $self->{spec} } ) ) { |
|
|
0
|
|
|
|
|
|
|
|
130
|
0
|
0
|
|
|
|
|
print "Installing $self->{spec}{$sub}{uri} as $self->{spec}{$sub}{name}\n" if $self->{debug}; |
|
131
|
|
|
|
|
|
|
Sub::Install::install_sub( |
|
132
|
|
|
|
|
|
|
{ |
|
133
|
|
|
|
|
|
|
code => sub { |
|
134
|
0
|
|
|
0
|
|
|
my $self = shift; |
|
135
|
0
|
|
|
|
|
|
return $self->_request( $sub, @_ ); |
|
136
|
|
|
|
|
|
|
}, |
|
137
|
0
|
0
|
|
|
|
|
as => $sub, |
|
138
|
|
|
|
|
|
|
into => "Selenium::Client", |
|
139
|
|
|
|
|
|
|
} |
|
140
|
|
|
|
|
|
|
) unless "Selenium::Client"->can($sub); |
|
141
|
|
|
|
|
|
|
} |
|
142
|
|
|
|
|
|
|
} |
|
143
|
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
#Check if server already up and spawn if no |
|
145
|
0
|
|
|
0
|
|
|
sub _spawn ($self) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
146
|
0
|
0
|
|
|
|
|
return $self->Status() if Net::EmptyPort::wait_port( $self->{port}, 1 ); |
|
147
|
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
# Pick a random port for the new server |
|
149
|
0
|
|
|
|
|
|
$self->{port} = Net::EmptyPort::empty_port(); |
|
150
|
|
|
|
|
|
|
|
|
151
|
0
|
|
|
|
|
|
my $driver_file = "Selenium/Driver/$self->{driver}.pm"; |
|
152
|
0
|
|
|
|
|
|
$driver_file =~ s/::/\//g; |
|
153
|
0
|
0
|
|
|
|
|
eval { require $driver_file } or confess "Could not load $driver_file, check your PERL5LIB: $@"; |
|
|
0
|
|
|
|
|
|
|
|
154
|
0
|
|
|
|
|
|
my $driver = "Selenium::Driver::$self->{driver}"; |
|
155
|
|
|
|
|
|
|
|
|
156
|
0
|
|
|
|
|
|
$driver->build_spawn_opts($self); |
|
157
|
0
|
|
|
|
|
|
return $self->_do_spawn(); |
|
158
|
|
|
|
|
|
|
} |
|
159
|
|
|
|
|
|
|
|
|
160
|
0
|
|
|
0
|
|
|
sub _do_spawn ($self) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
#XXX on windows we will *never* terminate if we are listening for *anything* |
|
163
|
|
|
|
|
|
|
#XXX so we have to just bg & ignore, unfortunately (also have to system()) |
|
164
|
0
|
0
|
|
|
|
|
if ( _is_windows() ) { |
|
165
|
0
|
|
|
|
|
|
$self->{pid} = qq/$self->{driver}:$self->{port}/; |
|
166
|
0
|
|
|
|
|
|
my @cmdprefix = ( "start /MIN", qq{"$self->{pid}"} ); |
|
167
|
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
# Selenium JAR controls it's own logging because Java |
|
169
|
0
|
|
|
|
|
|
my @cmdsuffix; |
|
170
|
0
|
0
|
|
|
|
|
@cmdsuffix = ( '>', $self->{log_file}, '2>&1' ) unless $self->{driver_class} eq 'Selenium::Driver::SeleniumHQ::Jar'; |
|
171
|
|
|
|
|
|
|
|
|
172
|
0
|
|
|
|
|
|
my $cmdstring = join( ' ', @cmdprefix, @{ $self->{command} }, @cmdsuffix ); |
|
|
0
|
|
|
|
|
|
|
|
173
|
0
|
0
|
|
|
|
|
print "$cmdstring\n" if $self->{debug}; |
|
174
|
0
|
|
|
|
|
|
system($cmdstring); |
|
175
|
0
|
|
|
|
|
|
return $self->_wait(); |
|
176
|
|
|
|
|
|
|
} |
|
177
|
|
|
|
|
|
|
|
|
178
|
0
|
0
|
|
|
|
|
print "@{$self->{command}}\n" if $self->{debug}; |
|
|
0
|
|
|
|
|
|
|
|
179
|
0
|
|
0
|
|
|
|
my $pid = fork // confess("Could not fork"); |
|
180
|
0
|
0
|
|
|
|
|
if ($pid) { |
|
181
|
0
|
|
|
|
|
|
$self->{pid} = $pid; |
|
182
|
0
|
|
|
|
|
|
return $self->_wait(); |
|
183
|
|
|
|
|
|
|
} |
|
184
|
0
|
|
|
|
|
|
open( my $fh, '>>', $self->{log_file} ); |
|
185
|
0
|
|
|
0
|
|
|
capture_merged { exec( @{ $self->{command} } ) } stdout => $fh; |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
} |
|
187
|
|
|
|
|
|
|
|
|
188
|
0
|
|
|
0
|
|
|
sub _wait ($self) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
189
|
0
|
0
|
|
|
|
|
print "Waiting for port to come up..." if $self->{debug}; |
|
190
|
0
|
0
|
|
|
|
|
Net::EmptyPort::wait_port( $self->{port}, 30 ) |
|
191
|
|
|
|
|
|
|
or confess("Server never came up on port $self->{port} after 30s!"); |
|
192
|
0
|
0
|
|
|
|
|
print "done\n" if $self->{debug}; |
|
193
|
0
|
|
|
|
|
|
return $self->Status(); |
|
194
|
|
|
|
|
|
|
} |
|
195
|
|
|
|
|
|
|
|
|
196
|
0
|
|
|
0
|
|
|
sub DESTROY ($self) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
197
|
0
|
0
|
|
|
|
|
return unless $self->{auto_close}; |
|
198
|
|
|
|
|
|
|
|
|
199
|
0
|
|
|
|
|
|
local $?; # Avoid affecting the exit status |
|
200
|
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
#Kill the server if we spawned one |
|
202
|
0
|
0
|
|
|
|
|
return unless $self->{pid}; |
|
203
|
0
|
0
|
|
|
|
|
print "Attempting to kill server process...\n" if $self->{debug}; |
|
204
|
|
|
|
|
|
|
|
|
205
|
0
|
0
|
|
|
|
|
if ( _is_windows() ) { |
|
206
|
0
|
|
|
|
|
|
my $killer = qq[taskkill /FI "WINDOWTITLE eq $self->{pid}"]; |
|
207
|
0
|
0
|
|
|
|
|
print "$killer\n" if $self->{debug}; |
|
208
|
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
#$killer .= ' > nul 2&>1' unless $self->{debug}; |
|
210
|
0
|
|
|
|
|
|
system($killer); |
|
211
|
0
|
|
|
|
|
|
return 1; |
|
212
|
|
|
|
|
|
|
} |
|
213
|
|
|
|
|
|
|
|
|
214
|
0
|
|
|
|
|
|
my $sig = 'TERM'; |
|
215
|
0
|
|
|
|
|
|
kill $sig, $self->{pid}; |
|
216
|
|
|
|
|
|
|
|
|
217
|
0
|
0
|
|
|
|
|
print "Issued SIG$sig to $self->{pid}, waiting...\n" if $self->{debug}; |
|
218
|
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
# 0 is always WCONTINUED, 1 is always WNOHANG, and POSIX is an expensive import |
|
220
|
|
|
|
|
|
|
# When 0 is returned, the process is still active, so it needs more persuasion |
|
221
|
0
|
|
|
|
|
|
foreach ( 0 .. 3 ) { |
|
222
|
0
|
0
|
|
|
|
|
return unless waitpid( $self->{pid}, 1 ) == 0; |
|
223
|
0
|
|
|
|
|
|
sleep 1; |
|
224
|
|
|
|
|
|
|
} |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
# Advanced persuasion |
|
227
|
0
|
0
|
|
|
|
|
print "Forcibly terminating selenium server process...\n" if $self->{debug}; |
|
228
|
0
|
|
|
|
|
|
kill( 'TERM', $self->{pid} ); |
|
229
|
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
#XXX unfortunately I can't just do a SIGALRM, because blocking system calls can't be intercepted on win32 |
|
231
|
0
|
|
|
|
|
|
foreach ( 0 .. $self->{timeout} ) { |
|
232
|
0
|
0
|
|
|
|
|
return unless waitpid( $self->{pid}, 1 ) == 0; |
|
233
|
0
|
|
|
|
|
|
sleep 1; |
|
234
|
|
|
|
|
|
|
} |
|
235
|
0
|
|
|
|
|
|
warn "Could not shut down selenium server!"; |
|
236
|
0
|
|
|
|
|
|
return; |
|
237
|
|
|
|
|
|
|
} |
|
238
|
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
sub _is_windows { |
|
240
|
0
|
|
|
0
|
|
|
return grep { $^O eq $_ } qw{msys MSWin32}; |
|
|
0
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
} |
|
242
|
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
#XXX some of the methods require content being null, some require it to be an obj with no params LOL |
|
244
|
|
|
|
|
|
|
our @bad_methods = qw{AcceptAlert DismissAlert Back Forward Refresh ElementClick MaximizeWindow MinimizeWindow FullscreenWindow SwitchToParentFrame ElementClear}; |
|
245
|
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
#Exempt some calls from return processing |
|
247
|
|
|
|
|
|
|
our @no_process = qw{Status GetAlertText GetTimeouts GetWindowRect GetElementRect GetAllCookies}; |
|
248
|
|
|
|
|
|
|
|
|
249
|
0
|
|
|
0
|
|
|
sub _request ( $self, $method, %params ) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
250
|
0
|
|
|
|
|
|
my $subject = $self->{spec}->{$method}; |
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
#TODO handle compressed output from server |
|
253
|
0
|
|
|
|
|
|
my %options = ( |
|
254
|
|
|
|
|
|
|
headers => { |
|
255
|
|
|
|
|
|
|
'Content-Type' => 'application/json; charset=utf-8', |
|
256
|
|
|
|
|
|
|
'Accept' => 'application/json; charset=utf-8', |
|
257
|
|
|
|
|
|
|
'Accept-Encoding' => 'identity', |
|
258
|
|
|
|
|
|
|
}, |
|
259
|
|
|
|
|
|
|
); |
|
260
|
0
|
0
|
|
|
|
|
$options{content} = '{}' if grep { $_ eq $method } @bad_methods; |
|
|
0
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
|
|
262
|
0
|
|
|
|
|
|
my $url = "$self->{scheme}://$self->{host}:$self->{port}$subject->{uri}"; |
|
263
|
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
# Remove parameters to inject into child objects |
|
265
|
0
|
0
|
|
|
|
|
my $inject_key = exists $params{inject} ? delete $params{inject} : undef; |
|
266
|
0
|
0
|
|
|
|
|
my $inject_value = $inject_key ? $params{$inject_key} : ''; |
|
267
|
0
|
|
|
|
|
|
my $inject; |
|
268
|
0
|
0
|
0
|
|
|
|
$inject = { to_inject => { $inject_key => $inject_value } } if $inject_key && $inject_value; |
|
269
|
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
# Keep sessions for passing to grandchildren |
|
271
|
0
|
0
|
|
|
|
|
$inject->{to_inject}{sessionid} = $params{sessionid} if exists $params{sessionid}; |
|
272
|
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
#If we have no extra params, and this is getSession, simplify |
|
274
|
0
|
0
|
0
|
|
|
|
%params = $self->_build_caps() if $method eq 'NewSession' && !%params; |
|
275
|
|
|
|
|
|
|
|
|
276
|
0
|
|
|
|
|
|
my @needed_params = $subject->{uri} =~ m/\{(\w+)\}/g; |
|
277
|
0
|
|
|
|
|
|
foreach my $param (@needed_params) { |
|
278
|
0
|
0
|
|
|
|
|
confess "$param is required for $method" unless exists $params{$param}; |
|
279
|
0
|
0
|
|
|
|
|
delete $params{$param} if $url =~ s/{\Q$param\E}/$params{$param}/g; |
|
280
|
|
|
|
|
|
|
} |
|
281
|
|
|
|
|
|
|
|
|
282
|
0
|
0
|
|
|
|
|
if (%params) { |
|
283
|
0
|
|
|
|
|
|
$options{content} = JSON::MaybeXS::encode_json( \%params ); |
|
284
|
0
|
|
|
|
|
|
$options{headers}{'Content-Length'} = length( $options{content} ); |
|
285
|
|
|
|
|
|
|
} |
|
286
|
|
|
|
|
|
|
|
|
287
|
0
|
0
|
|
|
|
|
print "$subject->{method} $url\n" if $self->{debug}; |
|
288
|
0
|
0
|
0
|
|
|
|
print "Body: $options{content}\n" if $self->{debug} && exists $options{content}; |
|
289
|
|
|
|
|
|
|
|
|
290
|
0
|
|
|
|
|
|
my $res = $self->{ua}->request( $subject->{method}, $url, \%options ); |
|
291
|
|
|
|
|
|
|
|
|
292
|
0
|
|
|
|
|
|
my @cbret; |
|
293
|
0
|
|
|
|
|
|
foreach my $cb ( @{ $self->{post_callbacks} } ) { |
|
|
0
|
|
|
|
|
|
|
|
294
|
0
|
0
|
0
|
|
|
|
if ( $cb && ref $cb eq 'CODE' ) { |
|
295
|
0
|
|
|
|
|
|
@options{qw{url method}} = ( $url, $subject->{method} ); |
|
296
|
0
|
0
|
|
|
|
|
$options{content} = \%params if %params; |
|
297
|
0
|
|
|
|
|
|
my $ret = $cb->( $self, $res, \%options ); |
|
298
|
0
|
0
|
|
|
|
|
push( @cbret, $ret ) if $ret; |
|
299
|
|
|
|
|
|
|
} |
|
300
|
0
|
0
|
|
|
|
|
return $cbret[0] if @cbret == 1; |
|
301
|
0
|
0
|
|
|
|
|
return @cbret if @cbret; |
|
302
|
|
|
|
|
|
|
} |
|
303
|
|
|
|
|
|
|
|
|
304
|
0
|
0
|
0
|
|
|
|
print "$res->{status} : $res->{content}\n" if $self->{debug} && ref $res eq 'HASH'; |
|
305
|
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
# all the selenium servers are UTF-8 |
|
307
|
0
|
|
|
|
|
|
my $normal = $res->{content}; |
|
308
|
0
|
0
|
|
|
|
|
$normal = NFC($normal) if $self->{normalize}; |
|
309
|
0
|
|
|
|
|
|
my $decoded_content = eval { JSON::MaybeXS->new()->utf8()->decode($normal) }; |
|
|
0
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
|
|
311
|
0
|
0
|
|
|
|
|
if ( $self->{fatal} ) { |
|
312
|
0
|
0
|
|
|
|
|
confess "$res->{reason} :\n Consult $subject->{href}\nRaw Error:\n$res->{content}\n" unless $res->{success}; |
|
313
|
|
|
|
|
|
|
} |
|
314
|
|
|
|
|
|
|
else { |
|
315
|
0
|
0
|
|
|
|
|
cluck "$res->{reason} :\n Consult $subject->{href}\nRaw Error:\n$res->{content}\n" unless $res->{success}; |
|
316
|
|
|
|
|
|
|
} |
|
317
|
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
#XXX should be caught below by objectify |
|
319
|
0
|
0
|
|
|
|
|
if ( grep { $method eq $_ } @no_process ) { |
|
|
0
|
|
|
|
|
|
|
|
320
|
0
|
0
|
|
|
|
|
if ( ref $decoded_content->{value} eq 'ARRAY' ) { |
|
321
|
0
|
0
|
|
|
|
|
return wantarray ? @{ $decoded_content->{value} } : $decoded_content->{value}; |
|
|
0
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
} |
|
323
|
0
|
|
|
|
|
|
return $decoded_content->{value}; |
|
324
|
|
|
|
|
|
|
} |
|
325
|
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
#XXX sigh |
|
327
|
0
|
0
|
|
|
|
|
if ( $decoded_content->{sessionId} ) { |
|
328
|
0
|
|
|
|
|
|
$decoded_content->{value} = [ { capabilities => $decoded_content->{value} }, { sessionId => $decoded_content->{sessionId} } ]; |
|
329
|
|
|
|
|
|
|
} |
|
330
|
|
|
|
|
|
|
|
|
331
|
0
|
|
|
|
|
|
return $self->_objectify( $decoded_content, $inject ); |
|
332
|
|
|
|
|
|
|
} |
|
333
|
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
our %classes = ( |
|
335
|
|
|
|
|
|
|
capabilities => { class => 'Selenium::Capabilities' }, |
|
336
|
|
|
|
|
|
|
sessionId => { |
|
337
|
|
|
|
|
|
|
class => 'Selenium::Session', |
|
338
|
|
|
|
|
|
|
destroy_callback => sub { |
|
339
|
|
|
|
|
|
|
my $self = shift; |
|
340
|
|
|
|
|
|
|
$self->DeleteSession( sessionid => $self->{sessionid} ) unless $self->{deleted}; |
|
341
|
|
|
|
|
|
|
}, |
|
342
|
|
|
|
|
|
|
callback => sub { |
|
343
|
|
|
|
|
|
|
my ( $self, $call ) = @_; |
|
344
|
|
|
|
|
|
|
$self->{deleted} = 1 if $call eq 'DeleteSession'; |
|
345
|
|
|
|
|
|
|
}, |
|
346
|
|
|
|
|
|
|
}, |
|
347
|
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
# Whoever thought this parameter name was a good idea... |
|
349
|
|
|
|
|
|
|
'element-6066-11e4-a52e-4f735466cecf' => { |
|
350
|
|
|
|
|
|
|
class => 'Selenium::Element', |
|
351
|
|
|
|
|
|
|
}, |
|
352
|
|
|
|
|
|
|
); |
|
353
|
|
|
|
|
|
|
|
|
354
|
0
|
|
|
0
|
|
|
sub _objectify ( $self, $result, $inject ) { |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
355
|
0
|
|
|
|
|
|
my $subject = $result->{value}; |
|
356
|
0
|
0
|
|
|
|
|
return $subject unless grep { ref $subject eq $_ } qw{ARRAY HASH}; |
|
|
0
|
|
|
|
|
|
|
|
357
|
0
|
0
|
|
|
|
|
$subject = [$subject] unless ref $subject eq 'ARRAY'; |
|
358
|
|
|
|
|
|
|
|
|
359
|
0
|
|
|
|
|
|
my @objs; |
|
360
|
0
|
|
|
|
|
|
foreach my $to_objectify (@$subject) { |
|
361
|
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
# If we have just data return it |
|
363
|
0
|
0
|
|
|
|
|
return wantarray ? @$subject : $subject if ref $to_objectify ne 'HASH'; |
|
|
|
0
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
|
|
365
|
0
|
|
|
|
|
|
my @objects = keys(%$to_objectify); |
|
366
|
0
|
|
|
|
|
|
foreach my $object (@objects) { |
|
367
|
|
|
|
|
|
|
|
|
368
|
0
|
|
|
|
|
|
my $has_class = exists $classes{$object}; |
|
369
|
|
|
|
|
|
|
|
|
370
|
0
|
|
0
|
|
|
|
my $base_object = $inject // {}; |
|
371
|
0
|
|
|
|
|
|
$base_object->{ lc($object) } = $to_objectify->{$object}; |
|
372
|
0
|
|
|
|
|
|
$base_object->{sortField} = lc($object); |
|
373
|
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
my $to_push = |
|
375
|
|
|
|
|
|
|
$has_class |
|
376
|
0
|
0
|
|
|
|
|
? $classes{$object}{class}->new( $self, $base_object ) |
|
377
|
|
|
|
|
|
|
: $to_objectify; |
|
378
|
0
|
|
|
|
|
|
$to_push->{sortField} = lc($object); |
|
379
|
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
# Save sessions for destructor |
|
381
|
0
|
0
|
|
|
|
|
push( @{ $self->{sessions} }, $to_push->session_id ) if ref $to_push eq 'Selenium::Session'; |
|
|
0
|
|
|
|
|
|
|
|
382
|
0
|
|
|
|
|
|
push( @objs, $to_push ); |
|
383
|
|
|
|
|
|
|
} |
|
384
|
|
|
|
|
|
|
} |
|
385
|
0
|
|
|
|
|
|
@objs = sort { $a->{sortField} cmp $b->{sortField} } @objs; |
|
|
0
|
|
|
|
|
|
|
|
386
|
0
|
0
|
|
|
|
|
return $objs[0] if @objs == 1; |
|
387
|
0
|
0
|
|
|
|
|
return wantarray ? @objs : \@objs; |
|
388
|
|
|
|
|
|
|
} |
|
389
|
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
1; |
|
391
|
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
package Selenium::Capabilities; |
|
394
|
|
|
|
|
|
|
$Selenium::Capabilities::VERSION = '2.01'; |
|
395
|
2
|
|
|
2
|
|
23
|
use parent qw{Selenium::Subclass}; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
21
|
|
|
396
|
|
|
|
|
|
|
1; |
|
397
|
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
package Selenium::Session; |
|
399
|
|
|
|
|
|
|
$Selenium::Session::VERSION = '2.01'; |
|
400
|
|
|
|
|
|
|
sub session_id { |
|
401
|
0
|
|
|
0
|
|
|
my $self = shift; |
|
402
|
0
|
|
0
|
|
|
|
return $self->{sessionId} // $self->{sessionid}; |
|
403
|
|
|
|
|
|
|
} |
|
404
|
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
sub DESTROY { |
|
406
|
0
|
|
|
0
|
|
|
my $self = shift; |
|
407
|
0
|
0
|
|
|
|
|
return if $self->{deleted}; |
|
408
|
0
|
|
|
|
|
|
$self->DeleteSession( sessionid => $self->session_id ); |
|
409
|
|
|
|
|
|
|
} |
|
410
|
|
|
|
|
|
|
|
|
411
|
2
|
|
|
2
|
|
386
|
use parent qw{Selenium::Subclass}; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
11
|
|
|
412
|
|
|
|
|
|
|
1; |
|
413
|
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
package Selenium::Element; |
|
415
|
|
|
|
|
|
|
$Selenium::Element::VERSION = '2.01'; |
|
416
|
2
|
|
|
2
|
|
159
|
use parent qw{Selenium::Subclass}; |
|
|
2
|
|
|
|
|
3
|
|
|
|
2
|
|
|
|
|
8
|
|
|
417
|
|
|
|
|
|
|
1; |
|
418
|
|
|
|
|
|
|
|
|
419
|
|
|
|
|
|
|
__END__ |
|
420
|
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
=pod |
|
422
|
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
=encoding UTF-8 |
|
424
|
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
=head1 NAME |
|
426
|
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
Selenium::Client - Module for communicating with WC3 standard selenium servers |
|
428
|
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
=head1 VERSION |
|
430
|
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
version 2.01 |
|
432
|
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
=head1 CONSTRUCTOR |
|
434
|
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
=head2 new(%options) = Selenium::Client |
|
436
|
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
Either connects to a driver at the specified host and port, or spawns one locally. |
|
438
|
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
Spawns a server on a random port in the event the host is "localhost" (or 127.0.0.1) and nothing is reachable on the provided port. |
|
440
|
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
Returns a Selenium::Client object with all WC3 methods exposed. |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
To view all available methods and their documentation, the catalog() method is provided. |
|
444
|
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
Remote Server options: |
|
446
|
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
=over 4 |
|
448
|
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
=item C<version> ENUM (stable,draft,unstable) - WC3 Spec to use. |
|
450
|
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
Default: stable |
|
452
|
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
=item C<host> STRING - hostname of your server. |
|
454
|
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
Default: localhost |
|
456
|
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
=item C<prefix> STRING - any prefix needed to communicate with the server, such as /wd, /hub, /wd/hub, or /grid |
|
458
|
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
Default: '' |
|
460
|
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
=item C<port> INTEGER - Port which the server is listening on. |
|
462
|
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
Default: 4444 |
|
464
|
|
|
|
|
|
|
Note: when spawning, this will be ignored and a random port chosen instead. |
|
465
|
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
=item C<scheme> ENUM (http,https) - HTTP scheme to use |
|
467
|
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
Default: http |
|
469
|
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
=item C<nofetch> BOOL - Do not check for a newer copy of the WC3 specifications on startup if we already have them available. |
|
471
|
|
|
|
|
|
|
|
|
472
|
|
|
|
|
|
|
Default: 1 |
|
473
|
|
|
|
|
|
|
|
|
474
|
|
|
|
|
|
|
=item C<client_dir> STRING - Where to store specs and other files downloaded when spawning servers. |
|
475
|
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
Default: ~/.selenium |
|
477
|
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
=item C<debug> BOOLEAN - Whether to print out various debugging output. |
|
479
|
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
Default: false |
|
481
|
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
=item C<auto_close> BOOLEAN - Automatically close spawned selenium servers and sessions. |
|
483
|
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
Only turn this off when you are debugging. |
|
485
|
|
|
|
|
|
|
|
|
486
|
|
|
|
|
|
|
Default: true |
|
487
|
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
=item C<normalize> BOOLEAN - Automatically normalize UTF-8 output using Normal Form C (NFC). |
|
489
|
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
If another normal form is preferred, you should turn this off and directly use L<Unicode::Normalize>. |
|
491
|
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
Default: true |
|
493
|
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
=item C<post_callbacks> ARRAY[CODE] - Executed after each request to the selenium server. |
|
495
|
|
|
|
|
|
|
|
|
496
|
|
|
|
|
|
|
Callbacks are passed $self, an HTTP::Tiny response hashref and the request hashref. |
|
497
|
|
|
|
|
|
|
Use this to implement custom error handlers, testing harness modifications etc. |
|
498
|
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
Return a truthy value to immediately exit the request subroutine after all cbs are executed. |
|
500
|
|
|
|
|
|
|
Truthy values (if any are returned) are returned in order encountered. |
|
501
|
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
=item C<fatal> BOOLEAN - Whether or not to die on errors from the selenium server. |
|
503
|
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
Default: true |
|
505
|
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
Useful to turn off when using post_callbacks as error handlers. |
|
507
|
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
=back |
|
509
|
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
When using remote servers, you should take extra care that they automatically clean up after themselves. |
|
511
|
|
|
|
|
|
|
We cannot guarantee the state of said servers after interacting with them. |
|
512
|
|
|
|
|
|
|
|
|
513
|
|
|
|
|
|
|
Spawn Options: |
|
514
|
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
=over 4 |
|
516
|
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
=item C<driver> STRING - Plug-in module used to spawn drivers when needed. |
|
518
|
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
Included are 'Auto', 'SeleniumHQ::Jar', 'Gecko', 'Chrome', 'Edge' |
|
520
|
|
|
|
|
|
|
Default: Auto |
|
521
|
|
|
|
|
|
|
|
|
522
|
|
|
|
|
|
|
The 'Auto' Driver will pick whichever direct driver looks like it will work for your chosen browser. |
|
523
|
|
|
|
|
|
|
If we can't find one, we'll fall back to SeleniumHQ::Jar. |
|
524
|
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
=item C<browser> STRING - desired browser. Used by the 'Auto' Driver. |
|
526
|
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
Default: Blank |
|
528
|
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
=item C<headless> BOOL - Whether to run the browser headless. Ignored by 'Safari' Driver. |
|
530
|
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
Default: True |
|
532
|
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
=item C<driver_version> STRING - Version of your driver software you wish to download and run. |
|
534
|
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
Blank and Partial versions will return the latest sub-version available. |
|
536
|
|
|
|
|
|
|
Only relevant to Drivers which auto-download (currently only SeleniumHQ::Jar). |
|
537
|
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
Default: Blank |
|
539
|
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
=back |
|
541
|
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
Driver modules should be in the Selenium::Driver namespace. |
|
543
|
|
|
|
|
|
|
They may implement additional parameters which can be passed into the options hash. |
|
544
|
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
=head1 METHODS |
|
546
|
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
=head2 Most of the methods are dynamic based on the selenium spec |
|
548
|
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
This means that the Selenium::Client class can directly call all selenium methods. |
|
550
|
|
|
|
|
|
|
We provide a variety of subclasses as sugar around this: |
|
551
|
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
Selenium::Session |
|
553
|
|
|
|
|
|
|
Selenium::Capabilities |
|
554
|
|
|
|
|
|
|
Selenium::Element |
|
555
|
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
Which will simplify correctly passing arguments in the case of sessions and elements. |
|
557
|
|
|
|
|
|
|
However, this does not change the fact that you still must take great care. |
|
558
|
|
|
|
|
|
|
We do no validation whatsoever of the inputs, and the selenium server likes to hang when you give it an invalid input. |
|
559
|
|
|
|
|
|
|
So take great care and understand this is what "script hung and died" means -- you passed the function an unrecognized argument. |
|
560
|
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
This is because Selenium::Specification cannot (yet!) parse the inputs and outputs for each endpoint at this time. |
|
562
|
|
|
|
|
|
|
As such we can't just filter against the relevant prototype. |
|
563
|
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
In any case, all subs will look like this, for example: |
|
565
|
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
$client->Method( key => value, key1 => value1, ...) = (@return_per_key) |
|
567
|
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
The options passed in are basically JSON serialized and passed directly as a POST body (or included into the relevant URL). |
|
569
|
|
|
|
|
|
|
We return a list of items which are a hashref per item in the result (some of them blessed). |
|
570
|
|
|
|
|
|
|
For example, NewSession will return a Selenium::Capabilities and Selenium::Session object. |
|
571
|
|
|
|
|
|
|
The order in which they are returned will be ordered alphabetically. |
|
572
|
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
=head2 Passing Capabilities to NewSession() |
|
574
|
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
By default, we will pass a set of capabilities that satisfy the options passed to new(). |
|
576
|
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
If you want *other* capabilities, pass them directly to NewSession as documented in the WC3 spec. |
|
578
|
|
|
|
|
|
|
|
|
579
|
|
|
|
|
|
|
However, this will ignore what you passed to new(). Caveat emptor. |
|
580
|
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
For the general list of options supported by each browser, see here: |
|
582
|
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
=over 4 |
|
584
|
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
=item C<Firefox> - https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions |
|
586
|
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
=item C<Chrome> - https://sites.google.com/a/chromium.org/chromedriver/capabilities |
|
588
|
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
=item C<Edge> - https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options |
|
590
|
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
=item C<Safari> - https://developer.apple.com/documentation/webkit/about_webdriver_for_safari |
|
592
|
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
=back |
|
594
|
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
=head2 catalog(BOOL verbose=0) = HASHREF |
|
596
|
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
Returns the entire method catalog. |
|
598
|
|
|
|
|
|
|
Prints out every method and a link to the relevant documentation if verbose is true. |
|
599
|
|
|
|
|
|
|
|
|
600
|
|
|
|
|
|
|
=head1 SUBCLASSES |
|
601
|
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
=head2 Selenium::Capabilities |
|
603
|
|
|
|
|
|
|
|
|
604
|
|
|
|
|
|
|
Returned as first element from NewSession(). |
|
605
|
|
|
|
|
|
|
Query this object for various things about the server capabilities. |
|
606
|
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
=head2 Selenium::Session |
|
608
|
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
Returned as second element of NewSession(). |
|
610
|
|
|
|
|
|
|
Has a destructor which will automatically clean itself up when we go out of scope. |
|
611
|
|
|
|
|
|
|
Alternatively, when the driver object goes out of scope, all sessions it spawned will be destroyed. |
|
612
|
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
You can call Selenium methods on this object which require a sessionid without passing it explicitly. |
|
614
|
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
=head2 Selenium::Element |
|
616
|
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
Returned from find element calls. |
|
618
|
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
You can call Selenium methods on this object which require a sessionid and elementid without passing them explicitly. |
|
620
|
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
=head1 STUPID SELENIUM TRICKS |
|
622
|
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
There are a variety of quirks with Selenium drivers that you just have to put up with, don't log bugs on these behaviors. |
|
624
|
|
|
|
|
|
|
Most of this will probably change in the future, |
|
625
|
|
|
|
|
|
|
as these are firmly in the "undefined/undocumented behavior" stack of the browser vendors. |
|
626
|
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
=head3 alerts |
|
628
|
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
If you have an alert() open on the page, all calls to the selenium server will 500 until you dismiss or accept it. |
|
630
|
|
|
|
|
|
|
|
|
631
|
|
|
|
|
|
|
Also be aware that chrome will re-fire alerts when you do a forward() or back() event, unlike firefox. |
|
632
|
|
|
|
|
|
|
|
|
633
|
|
|
|
|
|
|
=head3 tag names |
|
634
|
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
Safari returns ALLCAPS names for tags. amazing |
|
636
|
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
=head2 properties and attributes |
|
638
|
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
Many I<valid> properties/attributes will I<never> be accessible via GetProperty() or GetAttribute(). |
|
640
|
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
For example, getting the "for" value of a <label> element is flat-out impossible using either GetProperty or GetAttribute. |
|
642
|
|
|
|
|
|
|
There are many other such cases, the most common being "non-standard" properties such as aria-* or things used by JS templating engines. |
|
643
|
|
|
|
|
|
|
You are better off using JS shims to do any element inspection. |
|
644
|
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
Similarly the IsElementSelected() method is quite unreliable. |
|
646
|
|
|
|
|
|
|
We can work around this however by just using the CSS :checked pseudoselector when looking for elements, as that actually works. |
|
647
|
|
|
|
|
|
|
|
|
648
|
|
|
|
|
|
|
It is this for these reasons that you should consider abandoning Selenium for something that can actually do this correctly such as L<Playwright>. |
|
649
|
|
|
|
|
|
|
|
|
650
|
|
|
|
|
|
|
=head3 windows |
|
651
|
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
When closing windows, be aware you will be NOT be shot back to the last window you had focused before switching to the current one. |
|
653
|
|
|
|
|
|
|
You have to manually switch back to an existing one. |
|
654
|
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
Opening _blank targeted links *does not* automatically switch to the new window. |
|
656
|
|
|
|
|
|
|
The procedure for handling links of such a sort to do this is as follows: |
|
657
|
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
# Get current handle |
|
659
|
|
|
|
|
|
|
my $handle = $session->GetWindowHandle(); |
|
660
|
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
# Assuming the element is an href with target=_blank ... |
|
662
|
|
|
|
|
|
|
$element->ClickElement(); |
|
663
|
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
# Get all handles and filter for the ones that we aren't currently using |
|
665
|
|
|
|
|
|
|
my @handles = $session->GetWindowHandles(); |
|
666
|
|
|
|
|
|
|
my @new_handles = grep { $handle != $_ } @handles; |
|
667
|
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
# Use pop() as it will always be returned in the order windows are opened |
|
669
|
|
|
|
|
|
|
$session->SwitchToWindow( handle => pop(@new_handles) ); |
|
670
|
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
Different browser drivers also handle window handles differently. |
|
672
|
|
|
|
|
|
|
Chrome in particular demands you stringify handles returned from the driver. |
|
673
|
|
|
|
|
|
|
It also seems to be a lot less cooperative than firefox when setting the WindowRect. |
|
674
|
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
=head3 frames |
|
676
|
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
In the SwitchToFrame documentation, the claim is made that passing the element ID of a <frame> or <iframe> will switch the browsing context of the session to that frame. |
|
678
|
|
|
|
|
|
|
This is quite obviously false in every driver known. Example: |
|
679
|
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
# This does not ever work |
|
681
|
|
|
|
|
|
|
$session->SwitchToFrame( id => $session->FindElement( using => 'css selector', value => '#frame' )->{elementid} ); |
|
682
|
|
|
|
|
|
|
|
|
683
|
|
|
|
|
|
|
The only thing that actually works is switching by array index as you would get from window.frames in javascript: |
|
684
|
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
# Supposing #frame is the first frame encountered in the DOM, this works |
|
686
|
|
|
|
|
|
|
$session->SwitchToFrame( id => 0 ); |
|
687
|
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
As you might imagine this is a significant barrier to reliable automation as not every JS interperter will necessarily index in the same order. |
|
689
|
|
|
|
|
|
|
Nor is there, say, a GetFrames() method from which you could sensibly pick which one you want and move from there. |
|
690
|
|
|
|
|
|
|
The only workaround here would be to always execute a script to interrogate window.frames and guess which one you want based on the output of that. |
|
691
|
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
=head3 arguments |
|
693
|
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
If you make a request of the server with arguments it does not understand it will hang for 30s, so set a SIGALRM handler if you insist on doing so. |
|
695
|
|
|
|
|
|
|
|
|
696
|
|
|
|
|
|
|
=head2 MSWin32 issues |
|
697
|
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
The default version of the Java JRE from java.com is quite simply ancient on windows, and SeleniumHQ develops against JDK 11 and better. |
|
699
|
|
|
|
|
|
|
So make sure your JDK bin dir is in your PATH I<before> the JRE path (or don't install an ancient JRE lol) |
|
700
|
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
If you don't, you'll probably get insta-explosions due to their usage of new language features. |
|
702
|
|
|
|
|
|
|
Kind of like how you'll die if you use a perl without signatures with this module :) |
|
703
|
|
|
|
|
|
|
|
|
704
|
|
|
|
|
|
|
Also, due to perl pseudo-forks hanging forever if anything is ever waiting on read() in windows, we don't fork to spawn binaries. |
|
705
|
|
|
|
|
|
|
Instead we use C<start> to open a new cmd.exe window, which will show up in your task tray. |
|
706
|
|
|
|
|
|
|
Don't close this or your test will fail for obvious reasons. |
|
707
|
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
This also means that if you have to send ^C (SIGTERM) to your script or exit() prematurely, said window may be left dangling, |
|
709
|
|
|
|
|
|
|
as these behave a lot more like POSIX::_exit() does on unix systems. |
|
710
|
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
=head1 UTF-8 considerations |
|
712
|
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
The JSON responses from the selenium server are decoded as UTF-8, as per the Selenium standard. |
|
714
|
|
|
|
|
|
|
As a convenience, we automatically apply NFC to output via L<Unicode::Normalize>, which can be disabled by passing normalize=0 to the constructor. |
|
715
|
|
|
|
|
|
|
If you are comparing output from selenium calls against UTF-8 glyphs, `use utf8`, `use feature qw{unicode_strings}` and normalization is strongly suggested. |
|
716
|
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
=head1 AUTHOR |
|
718
|
|
|
|
|
|
|
|
|
719
|
|
|
|
|
|
|
George S. Baugh <george@troglodyne.net> |
|
720
|
|
|
|
|
|
|
|
|
721
|
|
|
|
|
|
|
=head1 BUGS |
|
722
|
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
Please report any bugs or feature requests on the bugtracker website |
|
724
|
|
|
|
|
|
|
L<https://github.com/troglodyne-internet-widgets/selenium-client-perl/issues> |
|
725
|
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
When submitting a bug or request, please include a test-file or a |
|
727
|
|
|
|
|
|
|
patch to an existing test-file that illustrates the bug or desired |
|
728
|
|
|
|
|
|
|
feature. |
|
729
|
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
=head1 AUTHORS |
|
731
|
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
Current Maintainers: |
|
733
|
|
|
|
|
|
|
|
|
734
|
|
|
|
|
|
|
=over 4 |
|
735
|
|
|
|
|
|
|
|
|
736
|
|
|
|
|
|
|
=item * |
|
737
|
|
|
|
|
|
|
|
|
738
|
|
|
|
|
|
|
George S. Baugh <george@troglodyne.net> |
|
739
|
|
|
|
|
|
|
|
|
740
|
|
|
|
|
|
|
=back |
|
741
|
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
=head1 CONTRIBUTORS |
|
743
|
|
|
|
|
|
|
|
|
744
|
|
|
|
|
|
|
=for stopwords Chris Faylor Manni Heumann |
|
745
|
|
|
|
|
|
|
|
|
746
|
|
|
|
|
|
|
=over 4 |
|
747
|
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
=item * |
|
749
|
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
Chris Faylor <cgf@realedsolutions.com> |
|
751
|
|
|
|
|
|
|
|
|
752
|
|
|
|
|
|
|
=item * |
|
753
|
|
|
|
|
|
|
|
|
754
|
|
|
|
|
|
|
Manni Heumann <heumann@strato.de> |
|
755
|
|
|
|
|
|
|
|
|
756
|
|
|
|
|
|
|
=back |
|
757
|
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
|
759
|
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
Copyright (c) 2024 Troglodyne LLC |
|
761
|
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
|
|
763
|
|
|
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
764
|
|
|
|
|
|
|
of this software and associated documentation files (the "Software"), to deal |
|
765
|
|
|
|
|
|
|
in the Software without restriction, including without limitation the rights |
|
766
|
|
|
|
|
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
767
|
|
|
|
|
|
|
copies of the Software, and to permit persons to whom the Software is |
|
768
|
|
|
|
|
|
|
furnished to do so, subject to the following conditions: |
|
769
|
|
|
|
|
|
|
The above copyright notice and this permission notice shall be included in all |
|
770
|
|
|
|
|
|
|
copies or substantial portions of the Software. |
|
771
|
|
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
772
|
|
|
|
|
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
773
|
|
|
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
774
|
|
|
|
|
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
775
|
|
|
|
|
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
776
|
|
|
|
|
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
777
|
|
|
|
|
|
|
SOFTWARE. |
|
778
|
|
|
|
|
|
|
|
|
779
|
|
|
|
|
|
|
=cut |