Client.pm 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. package Selenium::Client;
  2. # ABSTRACT: Module for communicating with WC3 standard selenium servers
  3. use strict;
  4. use warnings;
  5. use 5.006;
  6. use v5.28.0; # Before 5.006, v5.10.0 would not be understood.
  7. no warnings 'experimental';
  8. use feature qw/signatures/;
  9. use JSON::MaybeXS();
  10. use HTTP::Tiny();
  11. use Carp qw{confess cluck};
  12. use File::Path qw{make_path};
  13. use File::HomeDir();
  14. use File::Slurper();
  15. use File::Spec();
  16. use Sub::Install();
  17. use Net::EmptyPort();
  18. use Capture::Tiny qw{capture_merged};
  19. use Unicode::Normalize qw{NFC};
  20. use Selenium::Specification;
  21. =head1 CONSTRUCTOR
  22. =head2 new(%options) = Selenium::Client
  23. Either connects to a driver at the specified host and port, or spawns one locally.
  24. 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.
  25. Returns a Selenium::Client object with all WC3 methods exposed.
  26. To view all available methods and their documentation, the catalog() method is provided.
  27. Remote Server options:
  28. =over 4
  29. =item C<version> ENUM (stable,draft,unstable) - WC3 Spec to use.
  30. Default: stable
  31. =item C<host> STRING - hostname of your server.
  32. Default: localhost
  33. =item C<prefix> STRING - any prefix needed to communicate with the server, such as /wd, /hub, /wd/hub, or /grid
  34. Default: ''
  35. =item C<port> INTEGER - Port which the server is listening on.
  36. Default: 4444
  37. Note: when spawning, this will be ignored and a random port chosen instead.
  38. =item C<scheme> ENUM (http,https) - HTTP scheme to use
  39. Default: http
  40. =item C<nofetch> BOOL - Do not check for a newer copy of the WC3 specifications on startup if we already have them available.
  41. Default: 1
  42. =item C<client_dir> STRING - Where to store specs and other files downloaded when spawning servers.
  43. Default: ~/.selenium
  44. =item C<debug> BOOLEAN - Whether to print out various debugging output.
  45. Default: false
  46. =item C<auto_close> BOOLEAN - Automatically close spawned selenium servers and sessions.
  47. Only turn this off when you are debugging.
  48. Default: true
  49. =item C<normalize> BOOLEAN - Automatically normalize UTF-8 output using Normal Form C (NFC).
  50. If another normal form is preferred, you should turn this off and directly use L<Unicode::Normalize>.
  51. Default: true
  52. =item C<post_callbacks> ARRAY[CODE] - Executed after each request to the selenium server.
  53. Callbacks are passed $self, an HTTP::Tiny response hashref and the request hashref.
  54. Use this to implement custom error handlers, testing harness modifications etc.
  55. Return a truthy value to immediately exit the request subroutine after all cbs are executed.
  56. Truthy values (if any are returned) are returned in order encountered.
  57. =item C<fatal> BOOLEAN - Whether or not to die on errors from the selenium server.
  58. Default: true
  59. Useful to turn off when using post_callbacks as error handlers.
  60. =back
  61. When using remote servers, you should take extra care that they automatically clean up after themselves.
  62. We cannot guarantee the state of said servers after interacting with them.
  63. Spawn Options:
  64. =over 4
  65. =item C<driver> STRING - Plug-in module used to spawn drivers when needed.
  66. Included are 'Auto', 'SeleniumHQ::Jar', 'Gecko', 'Chrome', 'Edge'
  67. Default: Auto
  68. The 'Auto' Driver will pick whichever direct driver looks like it will work for your chosen browser.
  69. If we can't find one, we'll fall back to SeleniumHQ::Jar.
  70. =item C<browser> STRING - desired browser. Used by the 'Auto' Driver.
  71. Default: Blank
  72. =item C<headless> BOOL - Whether to run the browser headless. Ignored by 'Safari' Driver.
  73. Default: True
  74. =item C<driver_version> STRING - Version of your driver software you wish to download and run.
  75. Blank and Partial versions will return the latest sub-version available.
  76. Only relevant to Drivers which auto-download (currently only SeleniumHQ::Jar).
  77. Default: Blank
  78. =back
  79. Driver modules should be in the Selenium::Driver namespace.
  80. They may implement additional parameters which can be passed into the options hash.
  81. =cut
  82. sub new($class,%options) {
  83. $options{version} //= 'stable';
  84. $options{port} //= 4444;
  85. #XXX geckodriver doesn't bind to localhost lol
  86. $options{host} //= '127.0.0.1';
  87. $options{host} = '127.0.0.1' if $options{host} eq 'localhost';
  88. $options{nofetch} //= 1;
  89. $options{scheme} //= 'http';
  90. $options{prefix} //= '';
  91. $options{ua} //= HTTP::Tiny->new();
  92. $options{client_dir} //= File::HomeDir::my_home()."/.selenium";
  93. $options{driver} //= "SeleniumHQ::Jar";
  94. $options{post_callbacks} //= [];
  95. $options{auto_close} //= 1;
  96. $options{browser} //= '';
  97. $options{headless} //= 1;
  98. $options{normalize} //= 1;
  99. $options{fatal} //= 1;
  100. #create client_dir and log-dir
  101. my $dir = File::Spec->catdir( $options{client_dir},"perl-client" );
  102. make_path($dir);
  103. #Grab the spec
  104. $options{spec}= Selenium::Specification::read($options{client_dir},$options{version},$options{nofetch});
  105. my $self = bless(\%options, $class);
  106. $self->{sessions} = [];
  107. $self->_build_subs();
  108. $self->_spawn() if $options{host} eq '127.0.0.1';
  109. return $self;
  110. }
  111. =head1 METHODS
  112. =head2 Most of the methods are dynamic based on the selenium spec
  113. This means that the Selenium::Client class can directly call all selenium methods.
  114. We provide a variety of subclasses as sugar around this:
  115. Selenium::Session
  116. Selenium::Capabilities
  117. Selenium::Element
  118. Which will simplify correctly passing arguments in the case of sessions and elements.
  119. However, this does not change the fact that you still must take great care.
  120. We do no validation whatsoever of the inputs, and the selenium server likes to hang when you give it an invalid input.
  121. So take great care and understand this is what "script hung and died" means -- you passed the function an unrecognized argument.
  122. This is because Selenium::Specification cannot (yet!) parse the inputs and outputs for each endpoint at this time.
  123. As such we can't just filter against the relevant prototype.
  124. In any case, all subs will look like this, for example:
  125. $client->Method( key => value, key1 => value1, ...) = (@return_per_key)
  126. The options passed in are basically JSON serialized and passed directly as a POST body (or included into the relevant URL).
  127. We return a list of items which are a hashref per item in the result (some of them blessed).
  128. For example, NewSession will return a Selenium::Capabilities and Selenium::Session object.
  129. The order in which they are returned will be ordered alphabetically.
  130. =head2 Passing Capabilities to NewSession()
  131. By default, we will pass a set of capabilities that satisfy the options passed to new().
  132. If you want *other* capabilities, pass them directly to NewSession as documented in the WC3 spec.
  133. However, this will ignore what you passed to new(). Caveat emptor.
  134. For the general list of options supported by each browser, see here:
  135. =over 4
  136. =item C<Firefox> - https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
  137. =item C<Chrome> - https://sites.google.com/a/chromium.org/chromedriver/capabilities
  138. =item C<Edge> - https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options
  139. =item C<Safari> - https://developer.apple.com/documentation/webkit/about_webdriver_for_safari
  140. =back
  141. =head2 catalog(BOOL verbose=0) = HASHREF
  142. Returns the entire method catalog.
  143. Prints out every method and a link to the relevant documentation if verbose is true.
  144. =cut
  145. sub catalog($self,$printed=0) {
  146. return $self->{spec} unless $printed;
  147. foreach my $method (keys(%{$self->{spec}})) {
  148. print "$method: $self->{spec}{$method}{href}\n";
  149. }
  150. return $self->{spec};
  151. }
  152. my %browser_opts = (
  153. firefox => {
  154. name => 'moz:firefoxOptions',
  155. headless => sub ($c) {
  156. $c->{args} //= [];
  157. push(@{$c->{args}}, '-headless');
  158. },
  159. },
  160. chrome => {
  161. name => 'goog:chromeOptions',
  162. headless => sub ($c) {
  163. $c->{args} //= [];
  164. push(@{$c->{args}}, 'headless');
  165. },
  166. },
  167. MicrosoftEdge => {
  168. name =>'ms:EdgeOptions',
  169. headless => sub ($c) {
  170. $c->{args} //= [];
  171. push(@{$c->{args}}, 'headless');
  172. },
  173. },
  174. );
  175. sub _build_caps($self,%options) {
  176. $options{browser} = $self->{browser} if $self->{browser};
  177. $options{headless} = $self->{headless} if $self->{headless};
  178. my $c = {
  179. browserName => $options{browser},
  180. };
  181. my $browser = $browser_opts{$options{browser}};
  182. if ($browser) {
  183. my $browseropts = {};
  184. foreach my $k (keys %$browser) {
  185. next if $k eq 'name';
  186. $browser->{$k}->($browseropts) if $options{$k};
  187. }
  188. $c->{$browser->{name}} = $browseropts;
  189. }
  190. return (
  191. capabilities => {
  192. alwaysMatch => $c,
  193. },
  194. );
  195. }
  196. sub _build_subs($self) {
  197. foreach my $sub (keys(%{$self->{spec}})) {
  198. Sub::Install::install_sub(
  199. {
  200. code => sub {
  201. my $self = shift;
  202. return $self->_request($sub,@_);
  203. },
  204. as => $sub,
  205. into => "Selenium::Client",
  206. }
  207. ) unless "Selenium::Client"->can($sub);
  208. }
  209. }
  210. #Check if server already up and spawn if no
  211. sub _spawn($self) {
  212. return $self->Status() if Net::EmptyPort::wait_port( $self->{port}, 1 );
  213. # Pick a random port for the new server
  214. $self->{port} = Net::EmptyPort::empty_port();
  215. my $driver_file = "Selenium/Driver/$self->{driver}.pm";
  216. $driver_file =~ s/::/\//g;
  217. eval { require $driver_file } or confess "Could not load $driver_file, check your PERL5LIB: $@";
  218. my $driver = "Selenium::Driver::$self->{driver}";
  219. $driver->build_spawn_opts($self);
  220. return $self->_do_spawn();
  221. }
  222. sub _do_spawn($self) {
  223. #XXX on windows we will *never* terminate if we are listening for *anything*
  224. #XXX so we have to just bg & ignore, unfortunately (also have to system())
  225. if (_is_windows()) {
  226. $self->{pid} = qq/$self->{driver}:$self->{port}/;
  227. my @cmdprefix = ("start /MIN", qq{"$self->{pid}"});
  228. # Selenium JAR controls it's own logging because Java
  229. my @cmdsuffix;
  230. @cmdsuffix = ('>', $self->{log_file}, '2>&1') unless $self->{driver_class} eq 'Selenium::Driver::SeleniumHQ::Jar';
  231. my $cmdstring = join(' ', @cmdprefix, @{$self->{command}}, @cmdsuffix );
  232. print "$cmdstring\n" if $self->{debug};
  233. system($cmdstring);
  234. return $self->_wait();
  235. }
  236. print "@{$self->{command}}\n" if $self->{debug};
  237. my $pid = fork // confess("Could not fork");
  238. if ($pid) {
  239. $self->{pid} = $pid;
  240. return $self->_wait();
  241. }
  242. open(my $fh, '>>', $self->{log_file});
  243. capture_merged { exec(@{$self->{command}}) } stdout => $fh;
  244. }
  245. sub _wait ($self) {
  246. print "Waiting for port to come up..." if $self->{debug};
  247. Net::EmptyPort::wait_port( $self->{port}, 30 )
  248. or confess("Server never came up on port $self->{port} after 30s!");
  249. print "done\n" if $self->{debug};
  250. return $self->Status();
  251. }
  252. sub DESTROY($self) {
  253. return unless $self->{auto_close};
  254. local $?; # Avoid affecting the exit status
  255. print "Shutting down active sessions...\n" if $self->{debug};
  256. #murder all sessions we spawned so that die() cleans up properly
  257. if ($self->{ua} && @{$self->{sessions}}) {
  258. foreach my $session (@{$self->{sessions}}) {
  259. # An attempt was made. The session *might* already be dead.
  260. eval { $self->DeleteSession( sessionid => $session ) };
  261. }
  262. }
  263. #Kill the server if we spawned one
  264. return unless $self->{pid};
  265. print "Attempting to kill server process...\n" if $self->{debug};
  266. if (_is_windows()) {
  267. my $killer = qq[taskkill /FI "WINDOWTITLE eq $self->{pid}"];
  268. print "$killer\n" if $self->{debug};
  269. #$killer .= ' > nul 2&>1' unless $self->{debug};
  270. system($killer);
  271. return 1;
  272. }
  273. my $sig = 'TERM';
  274. kill $sig, $self->{pid};
  275. print "Issued SIG$sig to $self->{pid}, waiting...\n" if $self->{debug};
  276. # 0 is always WCONTINUED, 1 is always WNOHANG, and POSIX is an expensive import
  277. # When 0 is returned, the process is still active, so it needs more persuasion
  278. foreach (0..3) {
  279. return unless waitpid( $self->{pid}, 1) == 0;
  280. sleep 1;
  281. }
  282. # Advanced persuasion
  283. print "Forcibly terminating selenium server process...\n" if $self->{debug};
  284. kill('TERM', $self->{pid});
  285. #XXX unfortunately I can't just do a SIGALRM, because blocking system calls can't be intercepted on win32
  286. foreach (0..$self->{timeout}) {
  287. return unless waitpid( $self->{pid}, 1 ) == 0;
  288. sleep 1;
  289. }
  290. warn "Could not shut down selenium server!";
  291. return;
  292. }
  293. sub _is_windows {
  294. return grep { $^O eq $_ } qw{msys MSWin32};
  295. }
  296. #XXX some of the methods require content being null, some require it to be an obj with no params LOL
  297. our @bad_methods = qw{AcceptAlert DismissAlert Back Forward Refresh ElementClick MaximizeWindow MinimizeWindow FullscreenWindow SwitchToParentFrame ElementClear};
  298. #Exempt some calls from return processing
  299. our @no_process = qw{Status GetWindowRect GetElementRect GetAllCookies};
  300. sub _request($self, $method, %params) {
  301. my $subject = $self->{spec}->{$method};
  302. #TODO handle compressed output from server
  303. my %options = (
  304. headers => {
  305. 'Content-Type' => 'application/json; charset=utf-8',
  306. 'Accept' => 'application/json; charset=utf-8',
  307. 'Accept-Encoding' => 'identity',
  308. },
  309. );
  310. $options{content} = '{}' if grep { $_ eq $method } @bad_methods;
  311. my $url = "$self->{scheme}://$self->{host}:$self->{port}$subject->{uri}";
  312. # Remove parameters to inject into child objects
  313. my $inject_key = exists $params{inject} ? delete $params{inject} : undef;
  314. my $inject_value = $inject_key ? $params{$inject_key} : '';
  315. my $inject;
  316. $inject = { to_inject => { $inject_key => $inject_value } } if $inject_key && $inject_value;
  317. # Keep sessions for passing to grandchildren
  318. $inject->{to_inject}{sessionid} = $params{sessionid} if exists $params{sessionid};
  319. #If we have no extra params, and this is getSession, simplify
  320. %params = $self->_build_caps() if $method eq 'NewSession' && !%params;
  321. foreach my $param (keys(%params)) {
  322. confess "$param is required for $method" unless exists $params{$param};
  323. delete $params{$param} if $url =~ s/{\Q$param\E}/$params{$param}/g;
  324. }
  325. if (%params) {
  326. $options{content} = JSON::MaybeXS::encode_json(\%params);
  327. $options{headers}{'Content-Length'} = length($options{content});
  328. }
  329. print "$subject->{method} $url\n" if $self->{debug};
  330. print "Body: $options{content}\n" if $self->{debug} && exists $options{content};
  331. my $res = $self->{ua}->request($subject->{method}, $url, \%options);
  332. my @cbret;
  333. foreach my $cb (@{$self->{post_callbacks}}) {
  334. if ($cb && ref $cb eq 'CODE') {
  335. @options{qw{url method}} = ($url,$subject->{method});
  336. $options{content} = \%params if %params;
  337. my $ret = $cb->($self, $res, \%options);
  338. push(@cbret,$ret) if $ret;
  339. }
  340. return $cbret[0] if @cbret == 1;
  341. return @cbret if @cbret;
  342. }
  343. print "$res->{status} : $res->{content}\n" if $self->{debug} && ref $res eq 'HASH';
  344. # all the selenium servers are UTF-8
  345. my $normal = $res->{content};
  346. $normal = NFC( $normal ) if $self->{normalize};
  347. my $decoded_content = eval { JSON::MaybeXS->new()->utf8()->decode( $normal ) };
  348. if ($self->{fatal}) {
  349. confess "$res->{reason} :\n Consult $subject->{href}\nRaw Error:\n$res->{content}\n" unless $res->{success};
  350. } else {
  351. cluck "$res->{reason} :\n Consult $subject->{href}\nRaw Error:\n$res->{content}\n" unless $res->{success};
  352. }
  353. if (grep { $method eq $_ } @no_process) {
  354. return @{$decoded_content->{value}} if ref $decoded_content->{value} eq 'ARRAY';
  355. return $decoded_content->{value};
  356. }
  357. return $self->_objectify($decoded_content,$inject);
  358. }
  359. our %classes = (
  360. capabilities => { class => 'Selenium::Capabilities' },
  361. sessionId => {
  362. class => 'Selenium::Session',
  363. destroy_callback => sub {
  364. my $self = shift;
  365. $self->DeleteSession() unless $self->{deleted};
  366. },
  367. callback => sub {
  368. my ($self,$call) = @_;
  369. $self->{deleted} = 1 if $call eq 'DeleteSession';
  370. },
  371. },
  372. # Whoever thought this parameter name was a good idea...
  373. 'element-6066-11e4-a52e-4f735466cecf' => {
  374. class => 'Selenium::Element',
  375. },
  376. );
  377. sub _objectify($self,$result,$inject) {
  378. my $subject = $result->{value};
  379. return $subject unless grep { ref $subject eq $_ } qw{ARRAY HASH};
  380. $subject = [$subject] unless ref $subject eq 'ARRAY';
  381. my @objs;
  382. foreach my $to_objectify (@$subject) {
  383. # If we have just data return it
  384. return @$subject if ref $to_objectify ne 'HASH';
  385. my @objects = keys(%$to_objectify);
  386. foreach my $object (@objects) {
  387. my $has_class = exists $classes{$object};
  388. my $base_object = $inject // {};
  389. $base_object->{lc($object)} = $to_objectify->{$object};
  390. $base_object->{sortField} = lc($object);
  391. my $to_push = $has_class ?
  392. $classes{$object}{class}->new($self, $base_object ) :
  393. $to_objectify;
  394. $to_push->{sortField} = lc($object);
  395. # Save sessions for destructor
  396. push(@{$self->{sessions}}, $to_push->{sessionid}) if ref $to_push eq 'Selenium::Session';
  397. push(@objs,$to_push);
  398. }
  399. }
  400. @objs = sort { $a->{sortField} cmp $b->{sortField} } @objs;
  401. return $objs[0] if @objs == 1;
  402. return @objs;
  403. }
  404. 1;
  405. =head1 SUBCLASSES
  406. =head2 Selenium::Capabilities
  407. Returned as first element from NewSession().
  408. Query this object for various things about the server capabilities.
  409. =head2 Selenium::Session
  410. Returned as second element of NewSession().
  411. Has a destructor which will automatically clean itself up when we go out of scope.
  412. Alternatively, when the driver object goes out of scope, all sessions it spawned will be destroyed.
  413. You can call Selenium methods on this object which require a sessionid without passing it explicitly.
  414. =head2 Selenium::Element
  415. Returned from find element calls.
  416. You can call Selenium methods on this object which require a sessionid and elementid without passing them explicitly.
  417. =cut
  418. package Selenium::Capabilities;
  419. use parent qw{Selenium::Subclass};
  420. 1;
  421. package Selenium::Session;
  422. use parent qw{Selenium::Subclass};
  423. 1;
  424. package Selenium::Element;
  425. use parent qw{Selenium::Subclass};
  426. 1;
  427. __END__
  428. =head1 STUPID SELENIUM TRICKS
  429. There are a variety of quirks with Selenium drivers that you just have to put up with, don't log bugs on these behaviors.
  430. Most of this will probably change in the future,
  431. as these are firmly in the "undefined/undocumented behavior" stack of the browser vendors.
  432. =head3 alerts
  433. If you have an alert() open on the page, all calls to the selenium server will 500 until you dismiss or accept it.
  434. Also be aware that chrome will re-fire alerts when you do a forward() or back() event, unlike firefox.
  435. =head3 tag names
  436. Safari returns ALLCAPS names for tags. amazing
  437. =head2 properties and attributes
  438. Many I<valid> properties/attributes will I<never> be accessible via GetProperty() or GetAttribute().
  439. For example, getting the "for" value of a <label> element is flat-out impossible using either GetProperty or GetAttribute.
  440. There are many other such cases, the most common being "non-standard" properties such as aria-* or things used by JS templating engines.
  441. You are better off using JS shims to do any element inspection.
  442. Similarly the IsElementSelected() method is quite unreliable.
  443. We can work around this however by just using the CSS :checked pseudoselector when looking for elements, as that actually works.
  444. It is this for these reasons that you should consider abandoning Selenium for something that can actually do this correctly such as L<Playwright>.
  445. =head3 windows
  446. 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.
  447. You have to manually switch back to an existing one.
  448. Opening _blank targeted links *does not* automatically switch to the new window.
  449. The procedure for handling links of such a sort to do this is as follows:
  450. # Get current handle
  451. my $handle = $session->GetWindowHandle();
  452. # Assuming the element is an href with target=_blank ...
  453. $element->ClickElement();
  454. # Get all handles and filter for the ones that we aren't currently using
  455. my @handles = $session->GetWindowHandles();
  456. my @new_handles = grep { $handle != $_ } @handles;
  457. # Use pop() as it will always be returned in the order windows are opened
  458. $session->SwitchToWindow( handle => pop(@new_handles) );
  459. Different browser drivers also handle window handles differently.
  460. Chrome in particular demands you stringify handles returned from the driver.
  461. It also seems to be a lot less cooperative than firefox when setting the WindowRect.
  462. =head3 frames
  463. 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.
  464. This is quite obviously false in every driver known. Example:
  465. # This does not ever work
  466. $session->SwitchToFrame( id => $session->FindElement( using => 'css selector', value => '#frame' )->{elementid} );
  467. The only thing that actually works is switching by array index as you would get from window.frames in javascript:
  468. # Supposing #frame is the first frame encountered in the DOM, this works
  469. $session->SwitchToFrame( id => 0 );
  470. As you might imagine this is a significant barrier to reliable automation as not every JS interperter will necessarily index in the same order.
  471. Nor is there, say, a GetFrames() method from which you could sensibly pick which one you want and move from there.
  472. 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.
  473. =head3 arguments
  474. 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.
  475. =head2 MSWin32 issues
  476. The default version of the Java JRE from java.com is quite simply ancient on windows, and SeleniumHQ develops against JDK 11 and better.
  477. So make sure your JDK bin dir is in your PATH I<before> the JRE path (or don't install an ancient JRE lol)
  478. If you don't, you'll probably get insta-explosions due to their usage of new language features.
  479. Kind of like how you'll die if you use a perl without signatures with this module :)
  480. Also, due to perl pseudo-forks hanging forever if anything is ever waiting on read() in windows, we don't fork to spawn binaries.
  481. Instead we use C<start> to open a new cmd.exe window, which will show up in your task tray.
  482. Don't close this or your test will fail for obvious reasons.
  483. This also means that if you have to send ^C (SIGTERM) to your script or exit() prematurely, said window may be left dangling,
  484. as these behave a lot more like POSIX::_exit() does on unix systems.
  485. =head1 UTF-8 considerations
  486. The JSON responses from the selenium server are decoded as UTF-8, as per the Selenium standard.
  487. As a convenience, we automatically apply NFC to output via L<Unicode::Normalize>, which can be disabled by passing normalize=0 to the constructor.
  488. If you are comparing output from selenium calls against UTF-8 glyphs, `use utf8`, `use feature qw{unicode_strings}` and normalization is strongly suggested.
  489. =head1 AUTHOR
  490. George S. Baugh <george@troglodyne.net>