File Coverage

blib/lib/Selenium/Client/Driver.pm
Criterion Covered Total %
statement 39 301 12.9
branch 0 70 0.0
condition 0 25 0.0
subroutine 13 57 22.8
pod 31 37 83.7
total 83 490 16.9


line stmt bran cond sub pod time code
1             package Selenium::Client::Driver;
2             $Selenium::Client::Driver::VERSION = '2.01';
3 1     1   213998 use strict;
  1         3  
  1         46  
4 1     1   5 use warnings;
  1         2  
  1         90  
5              
6 1     1   8 use parent qw{Selenium::Remote::Driver};
  1         2  
  1         9  
7              
8 1     1   391576 no warnings qw{experimental};
  1         3  
  1         58  
9 1     1   7 use feature qw{signatures state};
  1         4  
  1         203  
10              
11             # ABSTRACT: Drop-In replacement for Selenium::Remote::Driver that supports selenium 4
12              
13 1     1   10 use Scalar::Util;
  1         2  
  1         75  
14 1     1   648 use Carp::Always;
  1         1038  
  1         5  
15 1     1   75 use Data::Dumper;
  1         5  
  1         64  
16 1     1   6 use JSON;
  1         3  
  1         39  
17              
18 1     1   893 use Selenium::Client;
  1         5  
  1         98  
19 1     1   649 use Selenium::Client::Commands;
  1         3  
  1         73  
20 1     1   669 use Selenium::Client::WebElement;
  1         4  
  1         51  
21 1     1   655 use Selenium::Client::WDKeys;
  1         4  
  1         8573  
22              
23              
24             #Getters/Setters
25              
26 0     0     sub _param ( $self, $default, $param, $value = undef ) {
  0            
  0            
  0            
  0            
  0            
27 0   0       $self->{$param} //= $default;
28 0 0         $self->{$param} = $value if defined $value;
29 0           return $self->{$param};
30             }
31              
32 0     0 1   sub driver ( $self, $driver = undef ) {
  0            
  0            
  0            
33 0           return $self->_param( undef, 'driver', $driver );
34             }
35              
36 0     0 1   sub base_url ( $self, $url = undef ) {
  0            
  0            
  0            
37 0           return $self->_param( '', 'base_url', $url );
38             }
39              
40 0     0 1   sub remote_server_addr ( $self, $addr = undef ) {
  0            
  0            
  0            
41 0           return $self->_param( 'localhost', 'remote_server_addr', $addr );
42             }
43              
44 0     0 1   sub port ( $self, $port = undef ) {
  0            
  0            
  0            
45 0           return $self->_param( 4444, 'port', $port );
46             }
47              
48 0     0 1   sub browser_name ( $self, $name = undef ) {
  0            
  0            
  0            
49 0           return $self->_param( 'firefox', 'browser_name', $name );
50             }
51              
52 0     0 1   sub platform ( $self, $platform = undef ) {
  0            
  0            
  0            
53 0           return $self->_param( 'ANY', 'platform', $platform );
54             }
55              
56 0     0 1   sub version ( $self, $version = undef ) {
  0            
  0            
  0            
57 0           return $self->_param( '', 'version', $version );
58             }
59              
60 0     0 1   sub webelement_class ( $self, $class = undef ) {
  0            
  0            
  0            
61 0           return $self->_param( 'Selenium::Client::WebElement', 'webelement_class', $class );
62             }
63              
64 0     0 1   sub default_finder ( $self, $finder = undef ) {
  0            
  0            
  0            
65 0           return $self->_param( 'xpath', 'default_finder', $finder );
66             }
67              
68 0     0 1   sub session ( $self, $session = undef ) {
  0            
  0            
  0            
69 0           return $self->_param( undef, 'session', $session );
70             }
71              
72 0     0 1   sub session_id ( $self, $id = undef ) {
  0            
  0            
  0            
73 0           return $self->session->{sessionId};
74             }
75              
76 0     0 0   sub remote_conn ( $self, $conn = undef ) {
  0            
  0            
  0            
77 0           return $self->_param( undef, 'remote_conn', $conn );
78             }
79              
80 0     0 1   sub error_handler ( $self, $handler = undef ) {
  0            
  0            
  0            
81             $handler //= sub {
82 0     0     my ( undef, $msg, $params ) = @_;
83 0           die "Internal Death: $msg\n" . Dumper($params);
84 0   0       };
85 0 0         die "error handler must be subroutine ref" unless ref $handler eq 'CODE';
86 0     0     return $self->_param( sub { }, 'error_handler', $handler );
87             }
88              
89 0     0 1   sub ua ( $self, $ua = undef ) {
  0            
  0            
  0            
90 0           return $self->_param( undef, 'ua', $ua );
91             }
92              
93 0     0 0   sub commands ($self) {
  0            
  0            
94 0   0       $self->{commands} //= Selenium::Client::Commands->new;
95 0           return $self->{commands};
96             }
97              
98 0     0 1   sub auto_close ( $self, $ac = undef ) {
  0            
  0            
  0            
99 0 0         return $self->_param( JSON::true, 'auto_close', $ac ? JSON::true : JSON::false );
100             }
101              
102             # Only here for compatibility
103             sub pid {
104 0     0 0   return $$;
105             }
106              
107             #TODO these bools may need JSONizing
108 0     0 1   sub javascript ( $self, $js = undef ) {
  0            
  0            
  0            
109 0 0         return $self->_param( JSON::true, 'javascript', $js ? JSON::true : JSON::false );
110             }
111              
112 0     0 1   sub accept_ssl_certs ( $self, $ssl = undef ) {
  0            
  0            
  0            
113 0 0         return $self->_param( JSON::true, 'accept_ssl_certs', $ssl ? JSON::true : JSON::false );
114             }
115              
116 0     0 1   sub proxy ( $self, $proxy = undef ) {
  0            
  0            
  0            
117 0 0         if ($proxy) {
118 0 0         die "Proxy must be a hashref" unless ref $proxy eq 'HASH';
119 0 0         if ( $proxy->{proxyType} =~ /^pac$/i ) {
120 0 0         if ( not defined $proxy->{proxyAutoconfigUrl} ) {
    0          
121 0           die "proxyAutoconfigUrl not provided\n";
122             }
123             elsif ( not( $proxy->{proxyAutoconfigUrl} =~ /^(http|file)/g ) ) {
124 0           die "proxyAutoconfigUrl should be of format http:// or file://";
125             }
126              
127 0 0         if ( $proxy->{proxyAutoconfigUrl} =~ /^file/ ) {
128 0           my $pac_url = $proxy->{proxyAutoconfigUrl};
129 0           my $file = $pac_url;
130 0           $file =~ s{^file://}{};
131              
132 0 0         if ( !-e $file ) {
133 0           die "proxyAutoConfigUrl file does not exist: '$pac_url'";
134             }
135             }
136             }
137             }
138 0           return $self->_param( undef, 'proxy', $proxy );
139             }
140              
141             #TODO what the hell is the difference between these two in practice?
142 0     0 1   sub extra_capabilities ( $self, $caps = undef ) {
  0            
  0            
  0            
143 0           return $self->_param( undef, 'extra_capabilities', $caps );
144             }
145              
146 0     0 0   sub desired_capabilities ( $self, $caps = undef ) {
  0            
  0            
  0            
147 0           return $self->_param( undef, 'desired_capabilities', $caps );
148             }
149              
150 0     0 0   sub capabilities ( $self, $caps = undef ) {
  0            
  0            
  0            
151 0           return $self->_param( undef, 'desired_capabilities', $caps );
152             }
153              
154 0     0 1   sub firefox_profile ( $self, $profile = undef ) {
  0            
  0            
  0            
155 0 0         if ($profile) {
156 0 0 0       unless ( Scalar::Util::blessed($profile) && $profile->isa('Selenium::Firefox::Profile') ) {
157 0           die "firefox_profile must be a Selenium::Firefox::Profile";
158             }
159             }
160 0           return $self->_param( undef, 'firefox_profile', $profile );
161             }
162              
163 0     0 1   sub debug ( $self, $debug = undef ) {
  0            
  0            
  0            
164 0           return $self->_param( 0, 'debug', $debug );
165             }
166              
167 0     0 0   sub headless ( $self, $headless = 0 ) {
  0            
  0            
  0            
168 0           return $self->_param( 0, 'headless', $headless );
169             }
170              
171 0     0 1   sub inner_window_size ( $self, $size = undef ) {
  0            
  0            
  0            
172 0 0         if ( ref $size eq 'ARRAY' ) {
173 0 0         die "inner_window_size must have two elements: [ height, width ]"
174             unless scalar @$size == 2;
175              
176 0           foreach my $dim (@$size) {
177 0 0         die 'inner_window_size only accepts integers, not: ' . $dim
178             unless Scalar::Util::looks_like_number($dim);
179             }
180              
181             }
182 0           return $self->_param( undef, 'inner_window_size', $size );
183             }
184              
185             # TODO do we care about this at all?
186             # At the time of writing, Geckodriver uses a different endpoint than
187             # the java bindings for executing synchronous and asynchronous
188             # scripts. As a matter of fact, Geckodriver does conform to the W3C
189             # spec, but as are bound to support both while the java bindings
190             # transition to full spec support, we need some way to handle the
191             # difference.
192 0     0     sub _execute_script_suffix ( $self, $suffix = undef ) {
  0            
  0            
  0            
193 0           return $self->_param( undef, '_execute_script_suffix', $suffix );
194             }
195              
196             #TODO generate find_element_by crap statically
197             #with 'Selenium::Remote::Finders';
198              
199 0     0 1   sub new ( $class, %options ) {
  0            
  0            
  0            
200 0           my $self = bless( { %options, is_wd3 => 1 }, $class );
201              
202             # map the options
203 0           my %optmap = (
204              
205             # SRD / common options
206             browser_name => 'browser',
207             debug => 'debug',
208             remote_server_addr => 'host',
209             port => 'port',
210             auto_close => 'auto_close',
211             ua => 'ua',
212              
213             # Stuff that does not work from SRD:
214              
215             # version - good luck getting random versions of browsers to work!!! LOL!!!!
216             # platform - TODO currently unsupported
217             # accept_ssl_certs - NOT IN THE SPEC, U CAN POUND SAND IF U AINT AN INTERMEDIATE SIGNER, HA HA HA HA.
218             # firefox_provile - NOT IN THE SPEC
219             # javascript - piss off, use mechanize if you want this off
220             # default_finder - TODO currently unsupported
221             # session_id - TODO currently unsupported
222             # pageLoadStrategy - NOT IN THE SPEC
223             # extra_capabilities - NOT IN THE SPEC
224             # base_url - TODO currently unsupported
225             # inner window size - XXX well, this function doesn't even work on selenium 4 so we *can't* support it.
226             # error_handler - TODO will probably have to shim this, may not be possible idk
227             # webelement_class- just no.
228             # proxy - TODO currently unsupported, not sure this is even in the spec.
229              
230             # SCD exclusive options
231             driver => 'driver',
232             driver_version => 'driver_version',
233             headless => 'headless',
234             fatal => 'fatal',
235             post_callbacks => 'post_callbacks',
236             normalize => 'normalize',
237             prefix => 'prefix',
238             scheme => 'scheme',
239             nofetch => 'nofetch',
240             client_dir => 'client_dir',
241             post_callbacks => 'post_callbacks', # TODO see error_handler note above
242             );
243              
244 0           my $driver = $self->driver();
245 0 0         if ( !$driver ) {
246              
247 0           my %actual;
248 0           foreach my $option ( keys(%options) ) {
249 0 0         if ( !exists $optmap{$option} ) {
250 0           warn "Passed unsupported option '$option', which has been dropped.";
251 0           next;
252             }
253 0           $actual{ $optmap{$option} } = $options{$option};
254             }
255              
256             # Set the version explicitly, as these are conflicting names between the two modules.
257 0           $actual{version} = 'stable';
258              
259 0           $driver = Selenium::Client->new(%actual);
260 0           $self->driver($driver);
261             }
262 0           my $status = $driver->Status();
263 0 0         die "Got bad status back from server!" unless $status->{ready};
264              
265 0 0         if ( !$self->session ) {
266 0 0         if ( $self->desired_capabilities ) {
267 0           $self->new_desired_session( $self->desired_capabilities );
268             }
269             else {
270             # Connect to remote server & establish a new session
271 0           $self->new_session( $self->extra_capabilities );
272             }
273             }
274              
275 0 0         if ( !( defined $self->session ) ) {
276 0           die "Could not establish a session with the remote server\n";
277             }
278              
279 0 0         if ( $self->inner_window_size ) {
280 0           my $size = $self->inner_window_size;
281 0           $self->set_inner_window_size(@$size);
282             }
283              
284             #Set debug if needed
285 0 0         $self->debug_on() if $self->debug;
286              
287 0           return $self;
288             }
289              
290 0     0 1   sub new_from_caps ( $self, %args ) {
  0            
  0            
  0            
291 0 0         if ( not exists $args{desired_capabilities} ) {
292 0           $args{desired_capabilities} = {};
293             }
294 0           return $self->new(%args);
295             }
296              
297             #TODO do we need this?
298       0     sub DESTROY {
299             }
300              
301             # This is an internal method used the Driver & is not supposed to be used by
302             # end user. This method is used by Driver to set up all the parameters
303             # (url & JSON), send commands & receive processed response from the server.
304 0     0     sub _execute_command ( $self, $res, $params = {} ) {
  0            
  0            
  0            
  0            
305 0 0         print "Executing $res->{command}\n" if $self->{debug};
306              
307             #XXX Sometimes the params are in $res. Whee.
308 0           foreach my $key ( keys(%$res) ) {
309 0 0         $params->{$key} = $res->{$key} unless grep { $key eq $_ } qw{command sessionid elementid};
  0            
310             }
311              
312 0 0         my $macguffin = $self->commands->needs_driver( $res->{command} ) ? $self->driver : $self->session;
313 0 0         $macguffin = $self->commands->needs_scd( $res->{command} ) ? $self : $macguffin;
314 0 0         die "Could not acquire driver/session!" unless $macguffin;
315 0           local $@;
316 0           my $result;
317             eval {
318 0           my $resp = $self->commands->request( $macguffin, $res->{command}, $params );
319 0           $result = $self->commands->parse_response( $macguffin, $res->{command}, $resp );
320 0           1;
321 0 0         } or do {
322 0 0         return $self->error_handler->( $macguffin, $@, { %$params, %$res } ) if $self->error_handler;
323 0           die $@;
324             };
325 0           return $result;
326             }
327              
328 0     0 1   sub has_javascript { 1 }
329              
330 0     0 1   sub new_session ( $self, $extra_capabilities = {} ) {
  0            
  0            
  0            
331 0   0       $extra_capabilities //= {};
332 0   0       my $caps = {
333             'platformName' => $self->platform,
334              
335             #'javascriptEnabled' => $self->javascript,
336             'version' => $self->version // '',
337             'acceptInsecureCerts' => $self->accept_ssl_certs,
338             %$extra_capabilities,
339             };
340              
341 0   0       $caps->{browserName} //= $self->browser_name;
342              
343 0 0         if ( defined $self->proxy ) {
344 0           $caps->{proxy} = $self->proxy;
345             }
346              
347 0 0 0       if ( $caps->{browserName}
      0        
348             && $caps->{browserName} =~ /firefox/i
349             && $self->firefox_profile ) {
350 0           $caps->{firefox_profile} = $self->firefox_profile->_encode;
351             }
352              
353 0           my %options = ( driver => 'auto', browser => $self->browser_name, debug => $self->debug, headless => $self->headless, capabilities => $caps );
354              
355 0           return $self->_request_new_session( \%options );
356             }
357              
358             sub new_desired_session {
359 0     0 1   my ( $self, $caps ) = @_;
360 0           return $self->new_session($caps);
361             }
362              
363             sub _request_new_session {
364 0     0     my ( $self, $args ) = @_;
365              
366 0           my $ret = $self->_execute_command( { command => 'newSession' }, $args->{capabilities} );
367 0           my ( $capabilities, $session ) = ( $ret->{capabilities}, $ret->{session} );
368              
369             #die "Failed to get caps back from newSession" unless $capabilities->isa("Selenium::Capabilities");
370             #die "Failed to get session back from newSession" unless $session->isa("Selenium::Session");
371 0           $self->session($session);
372 0           $self->capabilities($capabilities);
373              
374 0           return $self;
375             }
376              
377             sub is_webdriver_3 {
378 0     0 1   my $self = shift;
379 0           return $self->{is_wd3};
380             }
381              
382 0     0 1   sub debug_on ($self) {
  0            
  0            
383 0           $self->{debug} = 1;
384 0           $self->driver->{debug} = 1;
385             }
386              
387             sub debug_off {
388 0     0 1   my ($self) = @_;
389 0           $self->{debug} = 0;
390 0           $self->driver->{debug} = 0;
391             }
392              
393             sub get_sessions {
394 0     0 1   my ($self) = @_;
395 0           return $self->driver->{sessions};
396             }
397              
398             sub get_capabilities {
399 0     0 1   my $self = shift;
400 0           return $self->capabilities;
401             }
402              
403             1;
404              
405             __END__
406              
407             =pod
408              
409             =encoding UTF-8
410              
411             =head1 NAME
412              
413             Selenium::Client::Driver - Drop-In replacement for Selenium::Remote::Driver that supports selenium 4
414              
415             =head1 VERSION
416              
417             version 2.01
418              
419             =head1 DESCRIPTION
420              
421             Drop-in replacement for L<Selenium::Remote::Driver> which supports selenium 4.
422              
423             See the documentation for L<Selenium::Remote::Driver> for how to use this module unless otherwise noted below.
424              
425             Also, we support all valid L<Selenium::Client> constructor arguments, so you will likely want to consult those.
426              
427             There are also a number of constructor options from L<Selenium::Remote::Driver> which are either entirely incompatible with selenium 4, are unimplemented or were bad ideas in the first place:
428              
429             =over 4
430              
431             =item C<platform> - TODO. Will have to work in Selenium::Client first.
432              
433             =item C<default_finder> - TODO. Will need a shim in Selenium::Client::Commands.
434              
435             =item C<extra_capabilities> - TODO. Use the options relevant to Selenium::Client instead
436              
437             =item C<base_url> - TODO. Will have to work in Selenium::Client first.
438              
439             =item C<session_id> - TODO. I don't even know if you can do this with the W3C spec.
440              
441             =item C<inner_window_size> - TODO. This function doesn't work right on any browser so we could only do a "best effort" try.
442              
443             =item C<error_handler> - TODO. While post_callbacks are supported, there is no shim to make old error_handler subs work as post_callbacks.
444              
445             =item C<proxy> - TODO. not sure this is even possible with S4 caps.
446              
447             =item C<accept_ssl_certs> - Not in the W3C spec. Just make a self-signed CA and slap that sucker in /etc/ssl/certs, then use that to issue your self-signed certs.
448              
449             =item C<firefox_profile> - Not in the W3C spec. If you can't get it done with moz:firefoxOptions, it ain't getting done.
450              
451             =item C<pageLoadStrategy> - Not in the W3C spec. If you want to properly wait on page loads, you will need either a view-source based state-machine or executing scripts. Welcome to hell.
452              
453             =item C<webelement_class> - Subclass Selenium::Client::Driver instead
454              
455             =item C<javascript> - Are you really using selenium to disable javascript? Seek Help.
456              
457             =item C<version> - good luck getting random versions of browsers to work!!! LOL!!!! Playwright patches them rather than rely on perpetually broken driver binaries.
458              
459             =back
460              
461             Furthermore, selenium 4 totally fails at dealing with cookies and alerts.
462              
463             =head1 ALTERNATIVES
464              
465             My advice is to give up on this nonsense and...
466              
467             use Playwright;
468              
469             Instead. Or, wait until someone implements a WC3 compliant selenium server using playwright and we can end the madness.
470              
471             =head1 SEE ALSO
472              
473             Please see those modules/websites for more information related to this module.
474              
475             =over 4
476              
477             =item *
478              
479             L<Selenium::Client|Selenium::Client>
480              
481             =back
482              
483             =head1 BUGS
484              
485             Please report any bugs or feature requests on the bugtracker website
486             L<https://github.com/troglodyne-internet-widgets/selenium-client-perl/issues>
487              
488             When submitting a bug or request, please include a test-file or a
489             patch to an existing test-file that illustrates the bug or desired
490             feature.
491              
492             =head1 AUTHORS
493              
494             Current Maintainers:
495              
496             =over 4
497              
498             =item *
499              
500             George S. Baugh <george@troglodyne.net>
501              
502             =back
503              
504             =head1 COPYRIGHT AND LICENSE
505              
506             Copyright (c) 2024 Troglodyne LLC
507              
508              
509             Permission is hereby granted, free of charge, to any person obtaining a copy
510             of this software and associated documentation files (the "Software"), to deal
511             in the Software without restriction, including without limitation the rights
512             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
513             copies of the Software, and to permit persons to whom the Software is
514             furnished to do so, subject to the following conditions:
515             The above copyright notice and this permission notice shall be included in all
516             copies or substantial portions of the Software.
517             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
518             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
519             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
520             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
521             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
522             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
523             SOFTWARE.
524              
525             =cut