File Coverage

blib/lib/Selenium/Client.pm
Criterion Covered Total %
statement 61 279 21.8
branch 0 126 0.0
condition 0 59 0.0
subroutine 21 36 58.3
pod 2 2 100.0
total 84 502 16.7


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