Andy Baugh 2 роки тому
батько
коміт
229a0d3b1a
4 змінених файлів з 3508 додано та 3248 видалено
  1. 226 0
      at/sanity-v4.test
  2. 20 3243
      lib/Selenium/Remote/Driver.pm
  3. 3257 0
      lib/Selenium/Remote/Driver/v3.pm
  4. 5 5
      lib/Selenium/Remote/Driver/v4.pm

+ 226 - 0
at/sanity-v4.test

@@ -0,0 +1,226 @@
+use strict;
+use warnings;
+
+use Cwd qw{abs_path};
+use FindBin;
+
+use Test::More;
+use Test::Fatal;
+use Test::Deep;
+
+use Carp::Always;
+
+use Selenium::Remote::Driver;
+use Selenium::Remote::WDKeys;
+
+#TODO: cover new_from_caps
+#TODO: Selenium::Firefox::Profile usage
+
+my $driver = Selenium::Remote::Driver->new(
+    remote_server_addr => 'localhost',
+    port => 4444,
+    browser_name => 'firefox',
+    accept_ssl_certs => 1,
+    extra_capabilities => {
+        log => { level => 'trace' },
+    },
+);
+isa_ok($driver,'Selenium::Remote::Driver::v4',"Can get new S::R::D::v4") or die diag explain $driver;
+
+$driver->debug_on();
+
+is($driver->get_capabilities()->{browserName},'firefox',"Can get Capabilities correctly (WD3)");
+my $sessions = $driver->get_sessions();
+is(scalar(@$sessions),1,"Can fall back to selenium2 to list sessions");
+
+ok($driver->status()->{ready},"status reports OK (WD3)");
+
+#TODO do something about available_engines
+
+$driver->set_timeout('page load',10000);
+$driver->set_timeout('script',10000);
+$driver->set_timeout('implicit',10000);
+my $timeouts = $driver->get_timeouts();
+is($timeouts->{pageLoad},10000,"WD3 set/get timeouts works");
+is($timeouts->{script},10000,"WD3 set/get timeouts works");
+is($timeouts->{implicit},10000,"WD3 set/get timeouts works");
+
+$driver->set_async_script_timeout(20000);
+$driver->set_implicit_wait_timeout(5000);
+$timeouts = $driver->get_timeouts();
+is($timeouts->{script},20000,"WD3 shim for set_async timeouts works");
+is($timeouts->{implicit},5000,"WD3 shim for implicit timeouts works");
+
+my $loc = abs_path("$FindBin::Bin/test.html");
+ok($driver->get("file://$loc"),"Can load a web page (WD3)");
+
+is($driver->get_alert_text(),"BEEE DOOO","get_alert_text works (WD3)");
+is(exception { $driver->dismiss_alert() }, undef, "alert can be dismissed (WD3)");
+
+#This sucker wants "value" instead of "text" like in legacy
+ok($driver->send_keys_to_prompt("HORGLE"),"send_keys_to_prompt works (WD3)");
+is(exception { $driver->accept_alert() }, undef, "alert can be accepted (WD3)");
+
+my $handle = $driver->get_current_window_handle();
+ok($handle,"Got a window handle (WD3)");
+cmp_bag($driver->get_window_handles(),[$handle],"Can list window handles (WD3)");
+
+my $sz = $driver->get_window_size();
+ok(defined $sz->{height},"get_window_size works (WD3)");
+ok(defined $sz->{width},"get window size works (WD3)");
+my $pos = $driver->get_window_position();
+ok(defined $pos->{x},"get_window_size works (WD3)");
+ok(defined $pos->{y},"get window size works (WD3)");
+
+like($driver->get_current_url(),qr/test.html$/,"get_current_url works (WD3)");
+like($driver->get_title(),qr/test/i,"get_title works (WD3)");
+
+my $otherloc = abs_path("$FindBin::Bin/other.html");
+$driver->get("file://$otherloc");
+$driver->go_back();
+like($driver->get_title(),qr/test/i,"go_back works (WD3)");
+
+$driver->go_forward();
+like($driver->get_page_source(),qr/ZIPPY/,"go_forward & get_page_source works (WD3)");
+is(exception { $driver->refresh() }, undef, "refresh works (WD3)");
+$driver->go_back();
+
+#TODO execute_*_script testing
+
+ok($driver->screenshot(),"can get base64'd whole page screenshot (WD3)");
+ok($driver->find_element('body','tag_name')->screenshot(0),"can get element screenshot (WD3 ONLY) and find_element (WD3) works.");
+
+isa_ok($driver->find_element('red','class'),"Selenium::Remote::WebElement");
+isa_ok($driver->find_element('text','name'),"Selenium::Remote::WebElement");
+isa_ok($driver->find_element('Test Link', 'link_text'),"Selenium::Remote::WebElement");
+isa_ok($driver->find_element('Test', 'partial_link_text'),"Selenium::Remote::WebElement");
+
+is(scalar(@{$driver->find_elements('red','class')}),2,"can find multiple elements correctly");
+
+my $lem = $driver->find_element('body', 'tag_name');
+isa_ok($driver->find_child_element($lem, 'red','class'),"Selenium::Remote::WebElement");
+isa_ok($lem->child('red','class'),"Selenium::Remote::WebElement");
+isa_ok($driver->find_child_element($lem, 'text','name'),"Selenium::Remote::WebElement");
+isa_ok($driver->find_child_element($lem, 'Test Link', 'link_text'),"Selenium::Remote::WebElement");
+isa_ok($driver->find_child_element($lem, 'Test', 'partial_link_text'),"Selenium::Remote::WebElement");
+
+$lem = $driver->find_element('form','tag_name');
+is(scalar(@{$driver->find_child_elements($lem,'./*')}),6,"can find child elements (WD3)");
+is(scalar(@{$lem->children('./*')}),6,"can find child elements via children() alias (WD3)");
+
+isa_ok($driver->get_active_element(),"Selenium::Remote::WebElement");
+
+like(exception { $driver->cache_status() },qr/unknown command/, "cache_status unimplemented in WD3");
+like(exception {
+diag explain $driver->set_geolocation(location => {
+       latitude  => 40.714353,
+       longitude => -74.005973,
+       altitude  => 0.056747
+});
+}, qr/unknown command/, "set_geolocation unimplemented in WD3");
+like(exception { $driver->get_geolocation() }, qr/unknown command/, "get_geolocation unimplemented in WD3");
+
+ok($driver->get_log('server'), "get_log fallback works");
+ok( scalar(@{$driver->get_log_types()}),"can fallback for get_log_types");
+
+like(exception { $driver->set_orientation("LANDSCAPE") }, qr/unknown command/, "set_orientation unimplemented in WD3");
+like(exception { $driver->get_orientation() }, qr/unknown command/, "get_orientation unimplemented in WD3");
+
+like($driver->upload_file($otherloc),qr/other.html$/,"upload_file fallback works");
+
+#Jinkies, this stuff is cool, it prints the selenium server help page @_@
+like( exception { $driver->get_local_storage_item('whee') },qr/help/i,"get_local_storage_item prints help page");
+like( exception { $driver->delete_local_storage_item('whee') },qr/help/i,"get_local_storage_item prints help page");
+
+ok($driver->switch_to_frame($driver->find_element('frame', 'id')),"can switch to frame (WD3)");
+ok($driver->switch_to_frame(),"can switch to parent frame (WD3 only)");
+
+ok($driver->set_window_position(1,1),"can set window position (WD3)");
+ok($driver->set_window_size(640,480),"can set window size (WD3)");
+
+SKIP: {
+    skip(2, "maxi/mini not working right now?");
+    ok($driver->maximize_window(),"can maximize window (WD3)");
+    ok($driver->minimize_window(),"can minimize window (WD3 only)");
+}
+ok($driver->fullscreen_window(),"can fullscreen window (WD3 only)");
+
+is(scalar(@{$driver->get_all_cookies()}),1,"can get cookie list (WD3)");
+$driver->delete_all_cookies();
+is(scalar(@{$driver->get_all_cookies()}),0,"can delete all cookies (WD3)");
+
+ok($driver->mouse_move_to_location( element => $driver->find_element('a','tag_name')),"Can use new WD3 Actions API to emulate mouse_move_to_location");
+$driver->click();
+sleep 5;
+my $handles = $driver->get_window_handles();
+is(scalar(@$handles),2,"Can move to element and then click it correctly (WD3)");
+
+$driver->switch_to_window($handles->[1]);
+is(exception { $driver->close() }, undef, "Can close new window (WD3)");
+cmp_bag($driver->get_window_handles,[$handles->[0]],"Correct window closed (WD3)");
+$driver->switch_to_window($handles->[0]);
+
+my $input = $driver->find_element('input','tag_name');
+$driver->mouse_move_to_location( element => $input );
+$driver->click();
+#TODO pretty sure this isn't working right
+$driver->send_modifier('Shift','down');
+$driver->send_keys_to_active_element('howdy',KEYS->{tab});
+$input->send_keys('eee');
+$driver->mouse_move_to_location( element => $driver->find_element('body','tag_name'));
+$driver->click();
+
+#XXX this has to be a BUG in the driver, the keys are getting thru
+is($input->get_attribute('value'),'defaulthowdyeee',"element->get_attribute() emulates old behavior thru get_property (WD3)");
+is($input->get_attribute('value',1),'default',"element->get_attribute() can do it's actual job (WD3)");
+is($driver->execute_script(qq/ return document.querySelector('input').value /),'defaulthowdyeee',"execute_script works, and so does send_keys_to_active_element & element->send_keys (WD3)");
+$input->clear();
+is($input->get_property('value'),'',"clear() works (WD3)");
+
+is(exception { $driver->button_down() },undef,"Can button down (WD3)");
+is(exception { $driver->button_up() },undef,"Can button up (WD3)");
+is(exception { $driver->release_general_action() }, undef, "Can release_general_action (WD3)");
+
+ok($driver->find_element('radio2','id')->is_selected(),"WD3 is_selected() works");
+my $l1 = $driver->find_element('radio1','id');
+$l1->set_selected();
+$l1->set_selected();
+ok($l1->is_selected(),"WD3 set_selected works");
+$l1->toggle();
+ok(!$l1->is_selected(),"WD3 toggle works: off");
+$l1->toggle();
+ok($l1->is_selected(),"WD3 toggle works: on");
+
+my $l2 = $driver->find_element('hammertime','id');
+is( $l2->is_enabled(),0,"is_enabled works (WD3)");
+ok( $l2->get_element_location()->{x},"Can get element rect (WD3)");
+ok( $l2->get_size()->{'height'}, "Size shim on rect works (WD3)");
+is( $l2->get_tag_name(),'input',"get_tag_name works (WD3)");
+ok( defined $l2->get_element_location_in_view()->{x}, "get_element_location_in_view polyfill works (WD3)");
+
+is($driver->find_element('hidon','id')->is_displayed(),0,"is_displayed returns false for type=hidden elements");
+my $gone = $driver->find_element('no-see-em','id');
+is($gone->is_displayed(),0,"is_displayed returns false for display=none");
+is($gone->is_enabled(),1,"is_enabled returns true for non-input elements");
+
+is($driver->find_element('h1','tag_name')->get_text(),'Howdy Howdy Howdy', "get_text works (WD3)");
+
+$driver->find_element('clickme','id')->click();
+is(exception { $driver->dismiss_alert() }, undef, "Can click element (WD3)");
+
+$driver->find_element('form','tag_name')->submit();
+like($driver->get_page_source(),qr/ZIPPY/,"elem submit() works (WD3)");
+
+#Pretty sure this one has enough 'inertia' to not disappear all the sudden
+$driver->get('http://w3.org/History.html');
+$driver->add_cookie('foo','bar',undef,undef,0,0,time()+5000);
+is(scalar(@{$driver->get_all_cookies()}),1,"can set cookie (WD3)");
+
+is($driver->get_cookie_named('foo')->{value},'bar',"can get cookie by name (WD3 only)");
+
+$driver->delete_cookie_named('foo');
+is(scalar(@{$driver->get_all_cookies()}),0,"can delete named cookies (WD3)");
+
+is(exception { $driver->quit() }, undef, "Can quit (WD3)");
+
+done_testing();

+ 20 - 3243
lib/Selenium/Remote/Driver.pm

@@ -3,54 +3,9 @@ package Selenium::Remote::Driver;
 use strict;
 use warnings;
 
-# ABSTRACT: Perl Client for Selenium Remote Driver
-
-use Moo;
-use Try::Tiny;
-
-use 5.006;
-use v5.10.0;    # Before 5.006, v5.10.0 would not be understood.
-
-# See http://perldoc.perl.org/5.10.0/functions/use.html#use-VERSION
-# and http://www.dagolden.com/index.php/369/version-numbers-should-be-boring/
-# for details.
-
-use Carp;
-our @CARP_NOT;
-
-use IO::String;
-use Archive::Zip qw( :ERROR_CODES );
-use Scalar::Util;
 use Selenium::Remote::RemoteConnection;
-use Selenium::Remote::Commands;
-use Selenium::Remote::Spec;
-use Selenium::Remote::WebElement;
-use Selenium::Remote::WDKeys;
-use File::Spec::Functions ();
-use File::Basename qw(basename);
-use Sub::Install ();
-use MIME::Base64 ();
-use Time::HiRes qw(usleep);
-use Clone qw{clone};
-use List::Util qw{any};
-
-use constant FINDERS => {
-    class             => 'class name',
-    class_name        => 'class name',
-    css               => 'css selector',
-    id                => 'id',
-    link              => 'link text',
-    link_text         => 'link text',
-    name              => 'name',
-    partial_link_text => 'partial link text',
-    tag_name          => 'tag name',
-    xpath             => 'xpath',
-};
-
-our $FORCE_WD2            = 0;
-our $FORCE_WD3            = 0;
-our $FORCE_WD4            = 0;
-our %CURRENT_ACTION_CHAIN = ( actions => [] );
+
+# ABSTRACT: Perl Client for Selenium Remote Driver
 
 =for Pod::Coverage BUILD
 
@@ -547,3202 +502,24 @@ C<eval>, or use the parameterized versions find_element_*).
 
 =cut
 
-has 'remote_server_addr' => (
-    is     => 'rw',
-    coerce => sub { ( defined( $_[0] ) ? $_[0] : 'localhost' ) },
-    default   => sub { 'localhost' },
-    predicate => 1
-);
-
-has 'browser_name' => (
-    is     => 'rw',
-    coerce => sub { ( defined( $_[0] ) ? $_[0] : 'firefox' ) },
-    default => sub { 'firefox' },
-);
-
-has 'base_url' => (
-    is     => 'lazy',
-    coerce => sub {
-        my $base_url = shift;
-        $base_url =~ s|/$||;
-        return $base_url;
-    },
-    predicate => 'has_base_url',
-);
-
-has 'platform' => (
-    is     => 'rw',
-    coerce => sub { ( defined( $_[0] ) ? $_[0] : 'ANY' ) },
-    default => sub { 'ANY' },
-);
-
-has 'port' => (
-    is     => 'rw',
-    coerce => sub { ( defined( $_[0] ) ? $_[0] : '4444' ) },
-    default   => sub { '4444' },
-    predicate => 1
-);
-
-has 'version' => (
-    is      => 'rw',
-    default => sub { '' },
-);
-
-has 'webelement_class' => (
-    is      => 'rw',
-    default => sub { 'Selenium::Remote::WebElement' },
-);
-
-has 'default_finder' => (
-    is      => 'rw',
-    coerce  => sub { __PACKAGE__->FINDERS->{ $_[0] } },
-    default => sub { 'xpath' },
-);
-
-has 'session_id' => (
-    is      => 'rw',
-    default => sub { undef },
-);
-
-has 'remote_conn' => (
-    is      => 'lazy',
-    builder => sub {
-        my $self = shift;
-        return Selenium::Remote::RemoteConnection->new(
-            remote_server_addr => $self->remote_server_addr,
-            port               => $self->port,
-            ua                 => $self->ua,
-            wd_context_prefix  => $self->wd_context_prefix
-        );
-    },
-);
-
-has 'error_handler' => (
-    is     => 'rw',
-    coerce => sub {
-        my ($maybe_coderef) = @_;
-
-        if ( ref($maybe_coderef) eq 'CODE' ) {
-            return $maybe_coderef;
-        }
-        else {
-            croak 'The error handler must be a code ref.';
-        }
-    },
-    clearer   => 1,
-    predicate => 1
-);
-
-has 'ua' => (
-    is      => 'lazy',
-    builder => sub { return LWP::UserAgent->new }
-);
-
-has 'commands' => (
-    is      => 'lazy',
-    builder => sub {
-        return Selenium::Remote::Commands->new;
-    },
-);
-
-has 'commands_v3' => (
-    is      => 'lazy',
-    builder => sub {
-        return Selenium::Remote::Spec->new;
-    },
-);
-
-has 'auto_close' => (
-    is     => 'rw',
-    coerce => sub { ( defined( $_[0] ) ? $_[0] : 1 ) },
-    default => sub { 1 },
-);
-
-has 'pid' => (
-    is      => 'lazy',
-    builder => sub { return $$ }
-);
-
-has 'javascript' => (
-    is     => 'rw',
-    coerce => sub { $_[0] ? JSON::true : JSON::false },
-    default => sub { return JSON::true }
-);
-
-has 'accept_ssl_certs' => (
-    is     => 'rw',
-    coerce => sub { $_[0] ? JSON::true : JSON::false },
-    default => sub { return JSON::true }
-);
-
-has 'proxy' => (
-    is     => 'rw',
-    coerce => sub {
-        my $proxy = $_[0];
-        if ( $proxy->{proxyType} =~ /^pac$/i ) {
-            if ( not defined $proxy->{proxyAutoconfigUrl} ) {
-                croak "proxyAutoconfigUrl not provided\n";
-            }
-            elsif ( not( $proxy->{proxyAutoconfigUrl} =~ /^(http|file)/g ) ) {
-                croak
-                  "proxyAutoconfigUrl should be of format http:// or file://";
-            }
-
-            if ( $proxy->{proxyAutoconfigUrl} =~ /^file/ ) {
-                my $pac_url = $proxy->{proxyAutoconfigUrl};
-                my $file    = $pac_url;
-                $file =~ s{^file://}{};
-
-                if ( !-e $file ) {
-                    warn "proxyAutoConfigUrl file does not exist: '$pac_url'";
-                }
-            }
-        }
-        $proxy;
-    },
-);
-
-has 'extra_capabilities' => (
-    is      => 'rw',
-    default => sub { {} }
-);
-
-has 'firefox_profile' => (
-    is     => 'rw',
-    coerce => sub {
-        my $profile = shift;
-        unless ( Scalar::Util::blessed($profile)
-            && $profile->isa('Selenium::Firefox::Profile') )
-        {
-            croak "firefox_profile should be a Selenium::Firefox::Profile\n";
-        }
-
-        return $profile;
-    },
-    predicate => 'has_firefox_profile',
-    clearer   => 1
-);
-
-has debug => (
-    is => 'lazy',
-    default => sub { 0 },
-);
-
-has 'desired_capabilities' => (
-    is        => 'lazy',
-    predicate => 'has_desired_capabilities'
-);
-
-has 'inner_window_size' => (
-    is        => 'lazy',
-    predicate => 1,
-    coerce    => sub {
-        my $size = shift;
-
-        croak "inner_window_size must have two elements: [ height, width ]"
-          unless scalar @$size == 2;
-
-        foreach my $dim (@$size) {
-            croak 'inner_window_size only accepts integers, not: ' . $dim
-              unless Scalar::Util::looks_like_number($dim);
-        }
-
-        return $size;
-    },
-
-);
-
-# At the time of writing, Geckodriver uses a different endpoint than
-# the java bindings for executing synchronous and asynchronous
-# scripts. As a matter of fact, Geckodriver does conform to the W3C
-# spec, but as are bound to support both while the java bindings
-# transition to full spec support, we need some way to handle the
-# difference.
-
-has '_execute_script_suffix' => (
-    is      => 'lazy',
-    default => ''
-);
-
-with 'Selenium::Remote::Finders';
-with 'Selenium::Remote::Driver::CanSetWebdriverContext';
-
-sub BUILD {
-    my $self = shift;
-
-    if ( !( defined $self->session_id ) ) {
-        if ( $self->has_desired_capabilities ) {
-            $self->new_desired_session( $self->desired_capabilities );
-        }
-        else {
-            # Connect to remote server & establish a new session
-            $self->new_session( $self->extra_capabilities );
-        }
-    }
-
-    if ( !( defined $self->session_id ) ) {
-        croak "Could not establish a session with the remote server\n";
-    }
-    elsif ( $self->has_inner_window_size ) {
-        my $size = $self->inner_window_size;
-        $self->set_inner_window_size(@$size);
-    }
-
-    #Set debug if needed
-    $self->debug_on() if $self->debug;
-
-    # Setup non-croaking, parameter versions of finders
-    foreach my $by ( keys %{ $self->FINDERS } ) {
-        my $finder_name = 'find_element_by_' . $by;
-
-        # In case we get instantiated multiple times, we don't want to
-        # install into the name space every time.
-        unless ( $self->can($finder_name) ) {
-            my $find_sub = $self->_build_find_by($by);
-
-            Sub::Install::install_sub(
-                {
-                    code => $find_sub,
-                    into => __PACKAGE__,
-                    as   => $finder_name,
-                }
-            );
-        }
-    }
-}
-
-sub new_from_caps {
-    my ( $self, %args ) = @_;
-
-    if ( not exists $args{desired_capabilities} ) {
-        $args{desired_capabilities} = {};
-    }
-
-    return $self->new(%args);
-}
-
-sub DEMOLISH {
-    my ( $self, $in_global_destruction ) = @_;
-    return if $$ != $self->pid;
-    return if $in_global_destruction;
-    $self->quit() if ( $self->auto_close && defined $self->session_id );
-}
-
-# We install an 'around' because we can catch more exceptions this way
-# than simply wrapping the explicit croaks in _execute_command.
-# @args should be fed to the handler to provide context
-# return_value could be assigned from the handler if we want to allow the
-# error_handler to handle the errors
-
-around '_execute_command' => sub {
-    my $orig = shift;
-    my $self = shift;
-
-    # copy @_ because it gets lost in the way
-    my @args = @_;
-    my $return_value;
-    try {
-        $return_value = $orig->( $self, @args );
-    }
-    catch {
-        if ( $self->has_error_handler ) {
-            $return_value = $self->error_handler->( $self, $_, @args );
-        }
-        else {
-            croak $_;
-        }
-    };
-    return $return_value;
-};
-
-# This is an internal method used the Driver & is not supposed to be used by
-# end user. This method is used by Driver to set up all the parameters
-# (url & JSON), send commands & receive processed response from the server.
-sub _execute_command {
-    my ( $self, $res, $params ) = @_;
-    $res->{'session_id'} = $self->session_id;
-
-    print "Prepping $res->{command}\n" if $self->{debug};
-
-    #webdriver 3 shims
-    return $self->{capabilities}
-      if $res->{command} eq 'getCapabilities' && $self->{capabilities};
-    $res->{ms}    = $params->{ms}    if $params->{ms};
-    $res->{type}  = $params->{type}  if $params->{type};
-    $res->{text}  = $params->{text}  if $params->{text};
-    $res->{using} = $params->{using} if $params->{using};
-    $res->{value} = $params->{value} if $params->{value};
-
-    print "Executing $res->{command}\n" if $self->{debug};
-    my $resource =
-        $self->{is_wd3}
-      ? $self->commands_v3->get_params($res)
-      : $self->commands->get_params($res);
-
-    #Fall-back to legacy if wd3 command doesn't exist
-    if ( !$resource && $self->{is_wd3} ) {
-        print "Falling back to legacy selenium method for $res->{command}\n"
-          if $self->{debug};
-        $resource = $self->commands->get_params($res);
-    }
-
-    #XXX InternetExplorerDriver quirks
-    if ( $self->{is_wd3} && $self->browser_name eq 'internet explorer' ) {
-        delete $params->{ms};
-        delete $params->{type};
-        delete $resource->{payload}->{type};
-        my $oldvalue = delete $params->{'page load'};
-        $params->{pageLoad} = $oldvalue if $oldvalue;
-    }
-
-    if ($resource) {
-        $params = {} unless $params;
-        my $resp = $self->remote_conn->request( $resource, $params );
-
-#In general, the parse_response for v3 is better, which is why we use it *even if* we are falling back.
-        return $self->commands_v3->parse_response( $res, $resp )
-          if $self->{is_wd3};
-        return $self->commands->parse_response( $res, $resp );
-    }
-    else {
-        #Tell the use about the offending setting.
-        croak "Couldn't retrieve command settings properly ".$res->{command}."\n";
-    }
-}
-
-=head1 METHODS
-
-=head2 new_session (extra_capabilities)
-
-Make a new session on the server.
-Called by new(), not intended for regular use.
-
-Occaisonally handy for recovering from brower crashes.
-
-DANGER DANGER DANGER
-
-This will throw away your old session if you have not closed it!
-
-DANGER DANGER DANGER
-
-=cut
-
-sub new_session {
-    my ( $self, $extra_capabilities ) = @_;
-    $extra_capabilities ||= {};
-
-    my $args = {
-        'desiredCapabilities' => {
-            'browserName'       => $self->browser_name,
-            'platform'          => $self->platform,
-            'javascriptEnabled' => $self->javascript,
-            'version'           => $self->version // '',
-            'acceptSslCerts'    => $self->accept_ssl_certs,
-            %$extra_capabilities,
-        },
-    };
-    $args->{'extra_capabilities'} = \%$extra_capabilities unless $FORCE_WD2;
-
-    if ( defined $self->proxy ) {
-        $args->{desiredCapabilities}->{proxy} = $self->proxy;
-    }
-
-    if (   $args->{desiredCapabilities}->{browserName} =~ /firefox/i
-        && $self->has_firefox_profile )
-    {
-        $args->{desiredCapabilities}->{firefox_profile} =
-          $self->firefox_profile->_encode;
-    }
-
-    $self->_request_new_session($args);
-}
-
-=head2 new_desired_session(capabilities)
-
-Basically the same as new_session, but with caps.
-Sort of an analog to new_from_caps.
-
-=cut
-
-sub new_desired_session {
-    my ( $self, $caps ) = @_;
-
-    $self->_request_new_session(
-        {
-            desiredCapabilities => $caps
-        }
-    );
-}
-
-sub _request_new_session {
-    my ( $self, $args ) = @_;
-
-    #XXX UGLY shim for webdriver3
-    $args->{capabilities}->{alwaysMatch} =
-      clone( $args->{desiredCapabilities} );
-    my $cmap = $self->commands_v3->get_caps_map();
-    my $caps = $self->commands_v3->get_caps();
-    foreach my $cap ( keys( %{ $args->{capabilities}->{alwaysMatch} } ) ) {
-
-        #Handle browser specific capabilities
-        if ( exists( $args->{desiredCapabilities}->{browserName} )
-            && $cap eq 'extra_capabilities' )
-        {
-
-            if (
-                exists $args->{capabilities}->{alwaysMatch}
-                ->{'moz:firefoxOptions'}->{args} )
-            {
-                $args->{capabilities}->{alwaysMatch}->{$cap}->{args} =
-                  $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}
-                  ->{args};
-            }
-            $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'} =
-              $args->{capabilities}->{alwaysMatch}->{$cap}
-              if $args->{desiredCapabilities}->{browserName} eq 'firefox';
-
-#XXX the chrome documentation is lies, you can't do this yet
-#$args->{capabilities}->{alwaysMatch}->{'goog:chromeOptions'}      = $args->{capabilities}->{alwaysMatch}->{$cap} if $args->{desiredCapabilities}->{browserName} eq 'chrome';
-#Does not appear there are any MSIE based options, so let's just let that be
-        }
-        if (   exists( $args->{desiredCapabilities}->{browserName} )
-            && $args->{desiredCapabilities}->{browserName} eq 'firefox'
-            && $cap eq 'firefox_profile' )
-        {
-            if (
-                ref $args->{capabilities}->{alwaysMatch}->{$cap} eq
-                'Selenium::Firefox::Profile' )
-            {
-#XXX not sure if I need to keep a ref to the File::Temp::Tempdir object to prevent reaping
-                $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}
-                  ->{args} = [
-                    '-profile',
-                    $args->{capabilities}->{alwaysMatch}->{$cap}->{profile_dir}
-                      ->dirname()
-                  ];
-            }
-            else {
-           #previously undocumented feature that we can pass the encoded profile
-                $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}
-                  ->{profile} = $args->{capabilities}->{alwaysMatch}->{$cap};
-            }
-        }
-        foreach my $newkey ( keys(%$cmap) ) {
-            if ( $newkey eq $cap ) {
-                last if $cmap->{$newkey} eq $cap;
-                $args->{capabilities}->{alwaysMatch}->{ $cmap->{$newkey} } =
-                  $args->{capabilities}->{alwaysMatch}->{$cap};
-                delete $args->{capabilities}->{alwaysMatch}->{$cap};
-                last;
-            }
-        }
-        delete $args->{capabilities}->{alwaysMatch}->{$cap}
-          if !any { $_ eq $cap } @$caps;
-    }
-    delete $args->{desiredCapabilities}
-      if $FORCE_WD3;    #XXX fork working-around busted fallback in firefox
-    delete $args->{capabilities}
-      if $FORCE_WD2; #XXX 'secret' feature to help the legacy unit tests to work
-
-    #Delete compatibility layer when using drivers directly
-    if ( $self->isa('Selenium::Firefox') || $self->isa('Selenium::Chrome') || $self->isa('Selenium::Edge') ) {
-        if (   exists $args->{capabilities}
-            && exists $args->{capabilities}->{alwaysMatch} )
-        {
-            delete $args->{capabilities}->{alwaysMatch}->{browserName};
-            delete $args->{capabilities}->{alwaysMatch}->{browserVersion};
-            delete $args->{capabilities}->{alwaysMatch}->{platformName};
-        }
-    }
-
-    #Fix broken out of the box chrome because they hate the maintainers of their interfaces
-    if ( $self->isa('Selenium::Chrome') ) {
-        if ( exists $args->{desiredCapabilities} ) {
-            $args->{desiredCapabilities}{'goog:chromeOptions'}{args} //= [];
-            push(@{$args->{desiredCapabilities}{'goog:chromeOptions'}{args}}, qw{no-sandbox disable-dev-shm-usage});
-        }
-    }
-
-    # Die unless connection is good
-    my $rc = $self->remote_conn;
-    $rc->check_status();
-
-    # command => 'newSession' to fool the tests of commands implemented
-    # TODO: rewrite the testing better, this is so fragile.
-    my $resource_new_session = {
-        method => $self->commands->get_method('newSession'),
-        url    => $self->commands->get_url('newSession'),
-        no_content_success =>
-          $self->commands->get_no_content_success('newSession'),
-    };
-    my $resp = $rc->request( $resource_new_session, $args, );
-
-    if ( $resp->{cmd_status} && $resp->{cmd_status} eq 'NOT OK' ) {
-        croak "Could not obtain new session: ". $resp->{cmd_return}{message};
-    }
-
-    if ( ( defined $resp->{'sessionId'} ) && $resp->{'sessionId'} ne '' ) {
-        $self->session_id( $resp->{'sessionId'} );
-    }
-    else {
-        my $error = 'Could not create new session';
-
-        if ( ref $resp->{cmd_return} eq 'HASH' ) {
-            $error .= ': ' . $resp->{cmd_return}->{message};
-        }
-        else {
-            $error .= ': ' . $resp->{cmd_return};
-        }
-        croak $error;
-    }
-
-    #Webdriver 3 - best guess that this is 'whats goin on'
-    if ( ref $resp->{cmd_return} eq 'HASH'
-        && $resp->{cmd_return}->{capabilities} )
-    {
-        $self->{is_wd3}           = 1;
-        $self->{emulate_jsonwire} = 1;
-        $self->{capabilities}     = $resp->{cmd_return}->{capabilities};
-    }
-
-    #XXX chromedriver DOES NOT FOLLOW SPEC!
-    if ( ref $resp->{cmd_return} eq 'HASH' && $resp->{cmd_return}->{chrome} ) {
-        if ( defined $resp->{cmd_return}->{setWindowRect} )
-        {    #XXX i'm inferring we are wd3 based on the presence of this
-            $self->{is_wd3}           = 1;
-            $self->{emulate_jsonwire} = 1;
-            $self->{capabilities}     = $resp->{cmd_return};
-        }
-    }
-
-    #XXX unsurprisingly, neither does microsoft
-    if (   ref $resp->{cmd_return} eq 'HASH'
-        && $resp->{cmd_return}->{pageLoadStrategy}
-        && $self->browser_name eq 'MicrosoftEdge' )
-    {
-        $self->{is_wd3}           = 1;
-        $self->{emulate_jsonwire} = 1;
-        $self->{capabilities}     = $resp->{cmd_return};
-    }
-
-    return ( $args, $resp );
-}
-
-=head2 is_webdriver_3
-
-Print whether the server (or browser) thinks it's implemented webdriver 3.
-If this returns true, webdriver 3 methods will be used in the case an action exists in L<Selenium::Remote::Spec> for the method you are trying to call.
-If a method you are calling has no webdriver 3 equivalent (or browser extension), the legacy commands implemented in L<Selenium::Remote::Commands> will be used.
-
-Note how I said *thinks* above.  In the case you want to force usage of legacy methods, set $driver->{is_wd3} to work around various browser issues.
-
-=cut
-
-sub is_webdriver_3 {
-    my $self = shift;
-    return $self->{is_wd3};
-}
-
-=head2 debug_on
-
-  Description:
-    Turns on debugging mode and the driver will print extra info like request
-    and response to stdout. Useful, when you want to see what is being sent to
-    the server & what response you are getting back.
-
-  Usage:
-    $driver->debug_on;
-
-=cut
-
-sub debug_on {
-    my ($self) = @_;
-    $self->{debug} = 1;
-    $self->remote_conn->debug(1);
-}
-
-=head2 debug_off
-
-  Description:
-    Turns off the debugging mode.
-
-  Usage:
-    $driver->debug_off;
-
-=cut
-
-sub debug_off {
-    my ($self) = @_;
-    $self->{debug} = 0;
-    $self->remote_conn->debug(0);
-}
-
-=head2 get_sessions
-
-  Description:
-    Returns a list of the currently active sessions. Each session will be
-    returned as an array of Hashes with the following keys:
-
-    'id' : The session ID
-    'capabilities: An object describing session's capabilities
-
-  Output:
-    Array of Hashes
-
-  Usage:
-    print Dumper $driver->get_sessions();
-
-=cut
-
-sub get_sessions {
-    my ($self) = @_;
-    my $res = { 'command' => 'getSessions' };
-    return $self->_execute_command($res);
-}
-
-=head2 status
-
-  Description:
-    Query the server's current status. All server implementations
-    should return two basic objects describing the server's current
-    platform and when the server was built.
-
-  Output:
-    Hash ref
-
-  Usage:
-    print Dumper $driver->status;
-
-=cut
-
-sub status {
-    my ($self) = @_;
-    my $res = { 'command' => 'status' };
-    return $self->_execute_command($res);
-}
-
-=head2 get_alert_text
-
- Description:
-    Gets the text of the currently displayed JavaScript alert(), confirm()
-    or prompt() dialog.
-
- Example
-    my $string = $driver->get_alert_text;
-
-=cut
-
-sub get_alert_text {
-    my ($self) = @_;
-    my $res = { 'command' => 'getAlertText' };
-    return $self->_execute_command($res);
-}
-
-=head2 send_keys_to_active_element
-
- Description:
-    Send a sequence of key strokes to the active element. This command is
-    similar to the send keys command in every aspect except the implicit
-    termination: The modifiers are not released at the end of the call.
-    Rather, the state of the modifier keys is kept between calls, so mouse
-    interactions can be performed while modifier keys are depressed.
-
- Compatibility:
-    On webdriver 3 servers, don't use this to send modifier keys; use send_modifier instead.
-
- Input: 1
-    Required:
-        {ARRAY | STRING} - Array of strings or a string.
-
- Usage:
-    $driver->send_keys_to_active_element('abcd', 'efg');
-    $driver->send_keys_to_active_element('hijk');
-
-    or
-
-    # include the WDKeys module
-    use Selenium::Remote::WDKeys;
-    $driver->send_keys_to_active_element(KEYS->{'space'}, KEYS->{'enter'});
-
-=cut
-
-sub send_keys_to_active_element {
-    my ( $self, @strings ) = @_;
-
-    if ( $self->{is_wd3}
-        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
-    {
-        @strings = map { split( '', $_ ) } @strings;
-        my @acts = map {
-            (
-                {
-                    type  => 'keyDown',
-                    value => $_,
-                },
-                {
-                    type  => 'keyUp',
-                    value => $_,
-                }
-              )
-        } @strings;
-
-        my $action = {
-            actions => [
-                {
-                    id      => 'key',
-                    type    => 'key',
-                    actions => \@acts,
-                }
-            ]
-        };
-        return $self->general_action(%$action);
-    }
-
-    my $res    = { 'command' => 'sendKeysToActiveElement' };
-    my $params = { 'value'   => \@strings, };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 send_keys_to_alert
-
-Synonymous with send_keys_to_prompt
-
-=cut
-
-sub send_keys_to_alert {
-    return shift->send_keys_to_prompt(@_);
-}
-
-=head2 send_keys_to_prompt
-
- Description:
-    Sends keystrokes to a JavaScript prompt() dialog.
-
- Input:
-    {string} keys to send
-
- Example:
-    $driver->send_keys_to_prompt('hello world');
-  or
-    ok($driver->get_alert_text eq 'Please Input your name','prompt appears');
-    $driver->send_keys_to_alert("Larry Wall");
-    $driver->accept_alert;
-
-=cut
-
-sub send_keys_to_prompt {
-    my ( $self, $keys ) = @_;
-    my $res    = { 'command' => 'sendKeysToPrompt' };
-    my $params = { 'text'    => $keys };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 accept_alert
-
- Description:
-    Accepts the currently displayed alert dialog.  Usually, this is
-    equivalent to clicking the 'OK' button in the dialog.
-
- Example:
-    $driver->accept_alert;
-
-=cut
-
-sub accept_alert {
-    my ($self) = @_;
-    my $res = { 'command' => 'acceptAlert' };
-    return $self->_execute_command($res);
-}
-
-=head2 dismiss_alert
-
- Description:
-    Dismisses the currently displayed alert dialog. For comfirm()
-    and prompt() dialogs, this is equivalent to clicking the
-    'Cancel' button. For alert() dialogs, this is equivalent to
-    clicking the 'OK' button.
-
- Example:
-    $driver->dismiss_alert;
-
-=cut
-
-sub dismiss_alert {
-    my ($self) = @_;
-    my $res = { 'command' => 'dismissAlert' };
-    return $self->_execute_command($res);
-}
-
-=head2 general_action
-
-Provide an 'actions definition' hash to make webdriver use input devices.
-Given the spec for the structure of this data is 'non normative',
-it is left as an exercise to the reader what that means as to how to use this function.
-
-That said, it seems most of the data looks something like this:
-
-    $driver->general_action( actions => [{
-        type => 'pointer|key|none|somethingElseSuperSpecialDefinedByYourBrowserDriver',
-        id => MUST be mouse|key|none|other.  And by 'other' I mean anything else.  The first 3 are 'special' in that they are used in the global actions queue.
-              If you want say, another mouse action to execute in parallel to other mouse actions (to simulate multi-touch, for example), call your action 'otherMouseAction' or something.
-        parameters => {
-            someOption => "basically these are global parameters used by all steps in the forthcoming "action chain".
-        },
-        actions => [
-            {
-                type => "keyUp|KeyDown if key, pointerUp|pointerDown|pointerMove|pointerCancel if pointer, pause if any type",
-                key => A raw keycode or character from the keyboard if this is a key event,
-                duration => how many 'ticks' this action should take, you probably want this to be 0 all of the time unless you are evading Software debounce.
-                button => what number button if you are using a pointer (this sounds terribly like it might be re-purposed to be a joypad in the future sometime)
-                origin => Point of Origin if moving a pointer around
-                x => unit vector to travel along x-axis if pointerMove event
-                y => unit vector to travel along y-axis if pointerMove event
-            },
-            ...
-        ]
-        },
-        ...
-        ]
-    )
-
-Only available on WebDriver3 capable selenium servers.
-
-If you have called any legacy shim, such as mouse_move_to_location() previously, your actions passed will be appended to the existing actions queue.
-Called with no arguments, it simply executes the existing action queue.
-
-If you are looking for pre-baked action chains that aren't currently part of L<Selenium::Remote::Driver>,
-consider L<Selenium::ActionChains>, which is shipped with this distribution instead.
-
-=head3 COMPATIBILITY
-
-Like most places, the WC3 standard is openly ignored by the driver binaries.
-Generally an "actions" object will only accept:
-
-    { type => ..., value => ... }
-
-When using the direct drivers (E.G. Selenium::Chrome, Selenium::Firefox).
-This is not documented anywhere but here, as far as I can tell.
-
-=cut
-
-sub general_action {
-    my ( $self, %action ) = @_;
-
-    _queue_action(%action);
-    my $res = { 'command' => 'generalAction' };
-    my $out = $self->_execute_command( $res, \%CURRENT_ACTION_CHAIN );
-    %CURRENT_ACTION_CHAIN = ( actions => [] );
-    return $out;
-}
-
-sub _queue_action {
-    my (%action) = @_;
-    if ( ref $action{actions} eq 'ARRAY' ) {
-        foreach my $live_action ( @{ $action{actions} } ) {
-            my $existing_action;
-            foreach my $global_action ( @{ $CURRENT_ACTION_CHAIN{actions} } ) {
-                if ( $global_action->{id} eq $live_action->{id} ) {
-                    $existing_action = $global_action;
-                    last;
-                }
-            }
-            if ($existing_action) {
-                push(
-                    @{ $existing_action->{actions} },
-                    @{ $live_action->{actions} }
-                );
-            }
-            else {
-                push( @{ $CURRENT_ACTION_CHAIN{actions} }, $live_action );
-            }
-        }
-    }
-}
-
-=head2 release_general_action
-
-Nukes *all* input device state (modifier key up/down, pointer button up/down, pointer location, and other device state) from orbit.
-Call if you forget to do a *Up event in your provided action chains, or just to save time.
-
-Also clears the current actions queue.
-
-Only available on WebDriver3 capable selenium servers.
-
-=cut
-
-sub release_general_action {
-    my ($self) = @_;
-    my $res = { 'command' => 'releaseGeneralAction' };
-    %CURRENT_ACTION_CHAIN = ( actions => [] );
-    return $self->_execute_command($res);
-}
-
-=head2 mouse_move_to_location
-
- Description:
-    Move the mouse by an offset of the specificed element. If no
-    element is specified, the move is relative to the current mouse
-    cursor. If an element is provided but no offset, the mouse will be
-    moved to the center of the element. If the element is not visible,
-    it will be scrolled into view.
-
- Compatibility:
-    Due to limitations in the Webdriver 3 API, mouse movements have to be executed 'lazily' e.g. only right before a click() event occurs.
-    This is because there is no longer any persistent mouse location state; mouse movements are now totally atomic.
-    This has several problematic aspects; for one, I can't think of a way to both hover an element and then do another action relying on the element staying hover()ed,
-    Aside from using javascript workarounds.
-
- Output:
-    STRING -
-
- Usage:
-    # element - the element to move to. If not specified or is null, the offset is relative to current position of the mouse.
-    # xoffset - X offset to move to, relative to the top-left corner of the element. If not specified, the mouse will move to the middle of the element.
-    # yoffset - Y offset to move to, relative to the top-left corner of the element. If not specified, the mouse will move to the middle of the element.
-
-    print $driver->mouse_move_to_location(element => e, xoffset => x, yoffset => y);
-
-=cut
-
-sub mouse_move_to_location {
-    my ( $self, %params ) = @_;
-    $params{element} = $params{element}{id} if exists $params{element};
-
-    if ( $self->{is_wd3}
-        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
-    {
-        my $origin      = $params{element};
-        my $move_action = {
-            type     => "pointerMove",
-            duration => 0,
-            x        => $params{xoffset} // 0,
-            y        => $params{yoffset} // 0,
-        };
-        $move_action->{origin} =
-          { 'element-6066-11e4-a52e-4f735466cecf' => $origin }
-          if $origin;
-
-        _queue_action(
-            actions => [
-                {
-                    type         => "pointer",
-                    id           => 'mouse',
-                    "parameters" => { "pointerType" => "mouse" },
-                    actions      => [$move_action],
-                }
-            ]
-        );
-        return 1;
-    }
-
-    my $res = { 'command' => 'mouseMoveToLocation' };
-    return $self->_execute_command( $res, \%params );
-}
-
-=head2 move_to
-
-Synonymous with mouse_move_to_location
-
-=cut
-
-sub move_to {
-    return shift->mouse_move_to_location(@_);
-}
-
-=head2 get_capabilities
-
- Description:
-    Retrieve the capabilities of the specified session.
-
- Output:
-    HASH of all the capabilities.
-
- Usage:
-    my $capab = $driver->get_capabilities();
-    print Dumper($capab);
-
-=cut
-
-sub get_capabilities {
-    my $self = shift;
-    my $res = { 'command' => 'getCapabilities' };
-    return $self->_execute_command($res);
-}
-
-=head2 get_timeouts
-
-  Description:
-    Get the currently configured values (ms) for the page load, script and implicit timeouts.
-
-  Compatibility:
-    Only available on WebDriver3 enabled selenium servers.
-
-  Usage:
-    $driver->get_timeouts();
-
-=cut
-
-sub get_timeouts {
-    my $self = shift;
-    my $res = { 'command' => 'getTimeouts' };
-    return $self->_execute_command( $res, {} );
-}
-
-=head2 set_timeout
-
- Description:
-    Configure the amount of time that a particular type of operation can execute
-    for before they are aborted and a |Timeout| error is returned to the client.
-
- Input:
-    type - <STRING> - The type of operation to set the timeout for.
-                      Valid values are:
-                      "script"    : for script timeouts,
-                      "implicit"  : for modifying the implicit wait timeout
-                      "page load" : for setting a page load timeout.
-    ms - <NUMBER> - The amount of time, in milliseconds, that time-limited
-            commands are permitted to run.
-
- Usage:
-    $driver->set_timeout('script', 1000);
-
-=cut
-
-sub set_timeout {
-    my ( $self, $type, $ms ) = @_;
-    if ( not defined $type ) {
-        croak "Expecting type";
-    }
-    $ms   = _coerce_timeout_ms($ms);
-    $type = 'pageLoad'
-      if $type eq 'page load'
-      && $self->browser_name ne
-      'MicrosoftEdge';    #XXX SHIM they changed the WC3 standard mid stream
-
-    my $res    = { 'command' => 'setTimeout' };
-    my $params = { $type     => $ms };
-
-    #XXX edge still follows earlier versions of the WC3 standard
-    if ( $self->browser_name eq 'MicrosoftEdge' ) {
-        $params->{ms}   = $ms;
-        $params->{type} = $type;
-    }
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 set_async_script_timeout
-
- Description:
-    Set the amount of time, in milliseconds, that asynchronous scripts executed
-    by execute_async_script() are permitted to run before they are
-    aborted and a |Timeout| error is returned to the client.
-
- Input:
-    ms - <NUMBER> - The amount of time, in milliseconds, that time-limited
-            commands are permitted to run.
-
- Usage:
-    $driver->set_async_script_timeout(1000);
-
-=cut
-
-sub set_async_script_timeout {
-    my ( $self, $ms ) = @_;
-
-    return $self->set_timeout( 'script', $ms ) if $self->{is_wd3};
-
-    $ms = _coerce_timeout_ms($ms);
-    my $res    = { 'command' => 'setAsyncScriptTimeout' };
-    my $params = { 'ms'      => $ms };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 set_implicit_wait_timeout
-
- Description:
-    Set the amount of time the driver should wait when searching for elements.
-    When searching for a single element, the driver will poll the page until
-    an element is found or the timeout expires, whichever occurs first.
-    When searching for multiple elements, the driver should poll the page until
-    at least one element is found or the timeout expires, at which point it
-    will return an empty list. If this method is never called, the driver will
-    default to an implicit wait of 0ms.
-
-    This is exactly equivalent to calling L</set_timeout> with a type
-    arg of C<"implicit">.
-
- Input:
-    Time in milliseconds.
-
- Output:
-    Server Response Hash with no data returned back from the server.
-
- Usage:
-    $driver->set_implicit_wait_timeout(10);
-
-=cut
-
-sub set_implicit_wait_timeout {
-    my ( $self, $ms ) = @_;
-    return $self->set_timeout( 'implicit', $ms ) if $self->{is_wd3};
-
-    $ms = _coerce_timeout_ms($ms);
-    my $res    = { 'command' => 'setImplicitWaitTimeout' };
-    my $params = { 'ms'      => $ms };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 pause
-
- Description:
-    Pause execution for a specified interval of milliseconds.
-
- Usage:
-    $driver->pause(10000);  # 10 second delay
-    $driver->pause();       #  1 second delay default
-
- DEPRECATED: consider using Time::HiRes instead.
-
-=cut
-
-sub pause {
-    my $self = shift;
-    my $timeout = ( shift // 1000 ) * 1000;
-    usleep($timeout);
-}
-
-=head2 close
-
- Description:
-    Close the current window.
-
- Usage:
-    $driver->close();
- or
-    #close a popup window
-    my $handles = $driver->get_window_handles;
-    $driver->switch_to_window($handles->[1]);
-    $driver->close();
-    $driver->switch_to_window($handles->[0]);
-
-=cut
-
-sub close {
-    my $self = shift;
-    my $res = { 'command' => 'close' };
-    $self->_execute_command($res);
-}
-
-=head2 quit
-
- Description:
-    DELETE the session, closing open browsers. We will try to call
-    this on our down when we get destroyed, but in the event that we
-    are demolished during global destruction, we will not be able to
-    close the browser. For your own unattended and/or complicated
-    tests, we recommend explicitly calling quit to make sure you're
-    not leaving orphan browsers around.
-
-    Note that as a Moo class, we use a subroutine called DEMOLISH that
-    takes the place of DESTROY; for more information, see
-    https://metacpan.org/pod/Moo#DEMOLISH.
-
- Usage:
-    $driver->quit();
-
-=cut
-
-sub quit {
-    my $self = shift;
-    my $res = { 'command' => 'quit' };
-    $self->_execute_command($res);
-    $self->session_id(undef);
-}
-
-=head2 get_current_window_handle
-
- Description:
-    Retrieve the current window handle.
-
- Output:
-    STRING - the window handle
-
- Usage:
-    print $driver->get_current_window_handle();
-
-=cut
-
-sub get_current_window_handle {
-    my $self = shift;
-    my $res = { 'command' => 'getCurrentWindowHandle' };
-    return $self->_execute_command($res);
-}
-
-=head2 get_window_handles
-
- Description:
-    Retrieve the list of window handles used in the session.
-
- Output:
-    ARRAY of STRING - list of the window handles
-
- Usage:
-    print Dumper $driver->get_window_handles;
- or
-    # get popup, close, then back
-    my $handles = $driver->get_window_handles;
-    $driver->switch_to_window($handles->[1]);
-    $driver->close;
-    $driver->switch_to_window($handles->[0]);
-
-=cut
-
-sub get_window_handles {
-    my $self = shift;
-    my $res = { 'command' => 'getWindowHandles' };
-    return $self->_execute_command($res);
-}
-
-=head2 get_window_size
-
- Description:
-    Retrieve the window size
-
- Compatibility:
-    The ability to get the size of arbitrary handles by passing input only exists in WebDriver2.
-    You will have to switch to the window first going forward.
-
- Input:
-    STRING - <optional> - window handle (default is 'current' window)
-
- Output:
-    HASH - containing keys 'height' & 'width'
-
- Usage:
-    my $window_size = $driver->get_window_size();
-    print $window_size->{'height'}, $window_size->{'width'};
-
-=cut
-
-sub get_window_size {
-    my ( $self, $window ) = @_;
-    $window = ( defined $window ) ? $window : 'current';
-    my $res = { 'command' => 'getWindowSize', 'window_handle' => $window };
-    $res = { 'command' => 'getWindowRect', handle => $window }
-      if $self->{is_wd3};
-    return $self->_execute_command($res);
-}
-
-=head2 get_window_position
-
- Description:
-    Retrieve the window position
-
- Compatibility:
-    The ability to get the size of arbitrary handles by passing input only exists in WebDriver2.
-    You will have to switch to the window first going forward.
-
- Input:
-    STRING - <optional> - window handle (default is 'current' window)
-
- Output:
-    HASH - containing keys 'x' & 'y'
-
- Usage:
-    my $window_size = $driver->get_window_position();
-    print $window_size->{'x'}, $window_size->('y');
-
-=cut
-
-sub get_window_position {
-    my ( $self, $window ) = @_;
-    $window = ( defined $window ) ? $window : 'current';
-    my $res = { 'command' => 'getWindowPosition', 'window_handle' => $window };
-    $res = { 'command' => 'getWindowRect', handle => $window }
-      if $self->{is_wd3};
-    return $self->_execute_command($res);
-}
-
-=head2 get_current_url
-
- Description:
-    Retrieve the url of the current page
-
- Output:
-    STRING - url
-
- Usage:
-    print $driver->get_current_url();
-
-=cut
-
-sub get_current_url {
-    my $self = shift;
-    my $res = { 'command' => 'getCurrentUrl' };
-    return $self->_execute_command($res);
-}
-
-=head2 navigate
-
- Description:
-    Navigate to a given url. This is same as get() method.
-
- Input:
-    STRING - url
-
- Usage:
-    $driver->navigate('http://www.google.com');
-
-=cut
-
-sub navigate {
-    my ( $self, $url ) = @_;
-    $self->get($url);
-}
-
-=head2 get
-
- Description:
-    Navigate to a given url
-
- Input:
-    STRING - url
-
- Usage:
-    $driver->get('http://www.google.com');
-
-=cut
-
-sub get {
-    my ( $self, $url ) = @_;
-
-    if ( $self->has_base_url && $url !~ m|://| ) {
-        $url =~ s|^/||;
-        $url = $self->base_url . "/" . $url;
-    }
-
-    my $res    = { 'command' => 'get' };
-    my $params = { 'url'     => $url };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 get_title
-
- Description:
-    Get the current page title
-
- Output:
-    STRING - Page title
-
- Usage:
-    print $driver->get_title();
-
-=cut
-
-sub get_title {
-    my $self = shift;
-    my $res = { 'command' => 'getTitle' };
-    return $self->_execute_command($res);
-}
-
-=head2 go_back
-
- Description:
-    Equivalent to hitting the back button on the browser.
-
- Usage:
-    $driver->go_back();
-
-=cut
-
-sub go_back {
-    my $self = shift;
-    my $res = { 'command' => 'goBack' };
-    return $self->_execute_command($res);
-}
-
-=head2 go_forward
-
- Description:
-    Equivalent to hitting the forward button on the browser.
-
- Usage:
-    $driver->go_forward();
-
-=cut
-
-sub go_forward {
-    my $self = shift;
-    my $res = { 'command' => 'goForward' };
-    return $self->_execute_command($res);
-}
-
-=head2 refresh
-
- Description:
-    Reload the current page.
-
- Usage:
-    $driver->refresh();
-
-=cut
-
-sub refresh {
-    my $self = shift;
-    my $res = { 'command' => 'refresh' };
-    return $self->_execute_command($res);
-}
-
-=head2 has_javascript
-
- Description:
-    returns true if javascript is enabled in the driver.
-
- Compatibility:
-    Can't be false on WebDriver 3.
-
- Usage:
-    if ($driver->has_javascript) { ...; }
-
-=cut
-
-sub has_javascript {
-    my $self = shift;
-    return int( $self->javascript );
-}
-
-=head2 execute_async_script
-
- Description:
-    Inject a snippet of JavaScript into the page for execution in the context
-    of the currently selected frame. The executed script is assumed to be
-    asynchronous and must signal that is done by invoking the provided
-    callback, which is always provided as the final argument to the function.
-    The value to this callback will be returned to the client.
-
-    Asynchronous script commands may not span page loads. If an unload event
-    is fired while waiting for a script result, an error should be returned
-    to the client.
-
- Input: 2 (1 optional)
-    Required:
-        STRING - Javascript to execute on the page
-    Optional:
-        ARRAY - list of arguments that need to be passed to the script.
-
- Output:
-    {*} - Varied, depending on the type of result expected back from the script.
-
- Usage:
-    my $script = q{
-        var arg1 = arguments[0];
-        var callback = arguments[arguments.length-1];
-        var elem = window.document.findElementById(arg1);
-        callback(elem);
-    };
-    my $elem = $driver->execute_async_script($script,'myid');
-    $elem->click;
-
-=cut
-
-sub execute_async_script {
-    my ( $self, $script, @args ) = @_;
-    if ( $self->has_javascript ) {
-        if ( not defined $script ) {
-            croak 'No script provided';
-        }
-        my $res =
-          { 'command' => 'executeAsyncScript' . $self->_execute_script_suffix };
-
-        # Check the args array if the elem obj is provided & replace it with
-        # JSON representation
-        for ( my $i = 0 ; $i < @args ; $i++ ) {
-            if ( Scalar::Util::blessed( $args[$i] )
-                and $args[$i]->isa('Selenium::Remote::WebElement') )
-            {
-                if ( $self->{is_wd3} ) {
-                    $args[$i] =
-                      { 'element-6066-11e4-a52e-4f735466cecf' =>
-                          ( $args[$i] )->{id} };
-                }
-                else {
-                    $args[$i] = { 'ELEMENT' => ( $args[$i] )->{id} };
-                }
-            }
-        }
-
-        my $params = { 'script' => $script, 'args' => \@args };
-        my $ret = $self->_execute_command( $res, $params );
-
-        # replace any ELEMENTS with WebElement
-        if (    ref($ret)
-            and ( ref($ret) eq 'HASH' )
-            and $self->_looks_like_element($ret) )
-        {
-            $ret = $self->webelement_class->new(
-                id     => $ret,
-                driver => $self
-            );
-        }
-        return $ret;
-    }
-    else {
-        croak 'Javascript is not enabled on remote driver instance.';
-    }
-}
-
-=head2 execute_script
-
- Description:
-    Inject a snippet of JavaScript into the page and return its result.
-    WebElements that should be passed to the script as an argument should be
-    specified in the arguments array as WebElement object. Likewise,
-    any WebElements in the script result will be returned as WebElement object.
-
- Input: 2 (1 optional)
-    Required:
-        STRING - Javascript to execute on the page
-    Optional:
-        ARRAY - list of arguments that need to be passed to the script.
-
- Output:
-    {*} - Varied, depending on the type of result expected back from the script.
-
- Usage:
-    my $script = q{
-        var arg1 = arguments[0];
-        var elem = window.document.findElementById(arg1);
-        return elem;
-    };
-    my $elem = $driver->execute_script($script,'myid');
-    $elem->click;
-
-=cut
-
-sub execute_script {
-    my ( $self, $script, @args ) = @_;
-    if ( $self->has_javascript ) {
-        if ( not defined $script ) {
-            croak 'No script provided';
-        }
-        my $res =
-          { 'command' => 'executeScript' . $self->_execute_script_suffix };
-
-        # Check the args array if the elem obj is provided & replace it with
-        # JSON representation
-        for ( my $i = 0 ; $i < @args ; $i++ ) {
-            if ( Scalar::Util::blessed( $args[$i] )
-                and $args[$i]->isa('Selenium::Remote::WebElement') )
-            {
-                if ( $self->{is_wd3} ) {
-                    $args[$i] =
-                      { 'element-6066-11e4-a52e-4f735466cecf' =>
-                          ( $args[$i] )->{id} };
-                }
-                else {
-                    $args[$i] = { 'ELEMENT' => ( $args[$i] )->{id} };
-                }
-            }
-        }
-
-        my $params = { 'script' => $script, 'args' => [@args] };
-        my $ret = $self->_execute_command( $res, $params );
-
-        return $self->_convert_to_webelement($ret);
-    }
-    else {
-        croak 'Javascript is not enabled on remote driver instance.';
-    }
-}
-
-# _looks_like_element
-# An internal method to check if a return value might be an element
-
-sub _looks_like_element {
-    my ( $self, $maybe_element ) = @_;
-
-    return (
-             exists $maybe_element->{ELEMENT}
-          or exists $maybe_element->{'element-6066-11e4-a52e-4f735466cecf'}
-    );
-}
-
-# _convert_to_webelement
-# An internal method used to traverse a data structure
-# and convert any ELEMENTS with WebElements
-
-sub _convert_to_webelement {
-    my ( $self, $ret ) = @_;
-
-    if ( ref($ret) and ( ref($ret) eq 'HASH' ) ) {
-        if ( $self->_looks_like_element($ret) ) {
-
-            # replace an ELEMENT with WebElement
-            return $self->webelement_class->new(
-                id     => $ret,
-                driver => $self
-            );
-        }
-
-        my %hash;
-        foreach my $key ( keys %$ret ) {
-            $hash{$key} = $self->_convert_to_webelement( $ret->{$key} );
-        }
-        return \%hash;
-    }
-
-    if ( ref($ret) and ( ref($ret) eq 'ARRAY' ) ) {
-        my @array = map { $self->_convert_to_webelement($_) } @$ret;
-        return \@array;
-    }
-
-    return $ret;
-}
-
-=head2 screenshot
-
- Description:
-    Get a screenshot of the current page as a base64 encoded image.
-    Optionally pass {'full' => 1} as argument to take a full screenshot and not
-    only the viewport. (Works only with firefox and geckodriver >= 0.24.0)
-
- Output:
-    STRING - base64 encoded image
-
- Usage:
-    print $driver->screenshot();
-    print $driver->screenshot({'full' => 1});
-
-To conveniently write the screenshot to a file, see L</capture_screenshot>.
-
-=cut
-
-sub screenshot {
-    my ($self, $params) = @_;
-    $params //= { full => 0 };
-
-    croak "Full page screenshot only supported on geckodriver" if $params->{full} && ( $self->{browser_name} ne 'firefox' );
-
-    my $res = { 'command' => $params->{'full'} == 1 ? 'mozScreenshotFull' : 'screenshot' };
-    return $self->_execute_command($res);
-}
-
-=head2 capture_screenshot
-
- Description:
-    Capture a screenshot and save as a PNG to provided file name.
-    (The method is compatible with the WWW::Selenium method of the same name)
-    Optionally pass {'full' => 1} as second argument to take a full screenshot
-    and not only the viewport. (Works only with firefox and geckodriver >= 0.24.0)
-
- Output:
-    TRUE - (Screenshot is written to file)
-
- Usage:
-    $driver->capture_screenshot($filename);
-    $driver->capture_screenshot($filename, {'full' => 1});
-
-=cut
-
-sub capture_screenshot {
-    my ( $self, $filename, $params ) = @_;
-    croak '$filename is required' unless $filename;
-
-    open( my $fh, '>', $filename );
-    binmode $fh;
-    print $fh MIME::Base64::decode_base64( $self->screenshot($params) );
-    CORE::close $fh;
-    return 1;
-}
-
-=head2 available_engines
-
- Description:
-    List all available engines on the machine. To use an engine, it has to be present in this list.
-
- Compatibility:
-    Does not appear to be available on Webdriver3 enabled selenium servers.
-
- Output:
-    {Array.<string>} A list of available engines
-
- Usage:
-    print Dumper $driver->available_engines;
-
-=cut
-
-#TODO emulate behavior on wd3?
-#grep { eval { Selenium::Remote::Driver->new( browser => $_ ) } } (qw{firefox MicrosoftEdge chrome opera safari htmlunit iphone phantomjs},'internet_explorer');
-#might do the trick
-sub available_engines {
-    my ($self) = @_;
-    my $res = { 'command' => 'availableEngines' };
-    return $self->_execute_command($res);
-}
-
-=head2 switch_to_frame
-
- Description:
-    Change focus to another frame on the page. If the frame ID is null, the
-    server will switch to the page's default content. You can also switch to a
-    WebElement, for e.g. you can find an iframe using find_element & then
-    provide that as an input to this method. Also see e.g.
-
- Input: 1
-    Required:
-        {STRING | NUMBER | NULL | WebElement} - ID of the frame which can be one of the three
-                                   mentioned.
-
- Usage:
-    $driver->switch_to_frame('frame_1');
-    or
-    $driver->switch_to_frame($driver->find_element('iframe', 'tag_name'));
-
-=head3 COMPATIBILITY
-
-Chromedriver will vomit if you pass anything but a webElement, so you probably should do that from now on.
-
-=cut
-
-sub switch_to_frame {
-    my ( $self, $id ) = @_;
-
-    my $json_null = JSON::null;
-    my $params;
-    $id = ( defined $id ) ? $id : $json_null;
-
-    my $res = { 'command' => 'switchToFrame' };
-
-    if ( ref $id eq $self->webelement_class ) {
-        if ( $self->{is_wd3} ) {
-            $params =
-              { 'id' =>
-                  { 'element-6066-11e4-a52e-4f735466cecf' => $id->{'id'} } };
-        }
-        else {
-            $params = { 'id' => { 'ELEMENT' => $id->{'id'} } };
-        }
-    }
-    else {
-        $params = { 'id' => $id };
-    }
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 switch_to_parent_frame
-
-Webdriver 3 equivalent of calling switch_to_frame with no arguments (e.g. NULL frame).
-This is actually called in that case, supposing you are using WD3 capable servers now.
-
-=cut
-
-sub switch_to_parent_frame {
-    my ($self) = @_;
-    my $res = { 'command' => 'switchToParentFrame' };
-    return $self->_execute_command($res);
-}
-
-=head2 switch_to_window
-
- Description:
-    Change focus to another window. The window to change focus to may
-    be specified by its server assigned window handle, or by the value
-    of the page's window.name attribute.
-
-    If you wish to use the window name as the target, you'll need to
-    have set C<window.name> on the page either in app code or via
-    L</execute_script>, or pass a name as the second argument to the
-    C<window.open()> function when opening the new window. Note that
-    the window name used here has nothing to do with the window title,
-    or the C<< <title> >> element on the page.
-
-    Otherwise, use L</get_window_handles> and select a
-    Webdriver-generated handle from the output of that function.
-
- Input: 1
-    Required:
-        STRING - Window handle or the Window name
-
- Usage:
-    $driver->switch_to_window('MY Homepage');
- or
-    # close a popup window and switch back
-    my $handles = $driver->get_window_handles;
-    $driver->switch_to_window($handles->[1]);
-    $driver->close;
-    $driver->switch_to_window($handles->[0]);
-
-=cut
-
-sub switch_to_window {
-    my ( $self, $name ) = @_;
-    if ( not defined $name ) {
-        return 'Window name not provided';
-    }
-    my $res = { 'command' => 'switchToWindow' };
-    my $params = { 'name' => $name, 'handle' => $name };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 set_window_position
-
- Description:
-    Set the position (on screen) where you want your browser to be displayed.
-
- Compatibility:
-    In webDriver 3 enabled selenium servers, you may only operate on the focused window.
-    As such, the window handle argument below will be ignored in this context.
-
- Input:
-    INT - x co-ordinate
-    INT - y co-ordinate
-    STRING - <optional> - window handle (default is 'current' window)
-
- Output:
-    BOOLEAN - Success or failure
-
- Usage:
-    $driver->set_window_position(50, 50);
-
-=cut
-
-sub set_window_position {
-    my ( $self, $x, $y, $window ) = @_;
-    $window = ( defined $window ) ? $window : 'current';
-    if ( not defined $x and not defined $y ) {
-        croak "X & Y co-ordinates are required";
-    }
-    croak qq{Error: In set_window_size, argument x "$x" isn't numeric}
-      unless Scalar::Util::looks_like_number($x);
-    croak qq{Error: In set_window_size, argument y "$y" isn't numeric}
-      unless Scalar::Util::looks_like_number($y);
-    $x +=
-      0;  # convert to numeric if a string, otherwise they'll be sent as strings
-    $y += 0;
-    my $res = { 'command' => 'setWindowPosition', 'window_handle' => $window };
-    my $params = { 'x' => $x, 'y' => $y };
-    if ( $self->{is_wd3} ) {
-        $res = { 'command' => 'setWindowRect', handle => $window };
-    }
-    my $ret = $self->_execute_command( $res, $params );
-    return $ret ? 1 : 0;
-}
-
-=head2 set_window_size
-
- Description:
-    Set the size of the browser window
-
- Compatibility:
-    In webDriver 3 enabled selenium servers, you may only operate on the focused window.
-    As such, the window handle argument below will be ignored in this context.
-
- Input:
-    INT - height of the window
-    INT - width of the window
-    STRING - <optional> - window handle (default is 'current' window)
-
- Output:
-    BOOLEAN - Success or failure
-
- Usage:
-    $driver->set_window_size(640, 480);
-
-=cut
-
-sub set_window_size {
-    my ( $self, $height, $width, $window ) = @_;
-    $window = ( defined $window ) ? $window : 'current';
-    if ( not defined $height and not defined $width ) {
-        croak "height & width of browser are required";
-    }
-    croak qq{Error: In set_window_size, argument height "$height" isn't numeric}
-      unless Scalar::Util::looks_like_number($height);
-    croak qq{Error: In set_window_size, argument width "$width" isn't numeric}
-      unless Scalar::Util::looks_like_number($width);
-    $height +=
-      0;  # convert to numeric if a string, otherwise they'll be sent as strings
-    $width += 0;
-    my $res = { 'command' => 'setWindowSize', 'window_handle' => $window };
-    my $params = { 'height' => $height, 'width' => $width };
-    if ( $self->{is_wd3} ) {
-        $res = { 'command' => 'setWindowRect', handle => $window };
-    }
-    my $ret = $self->_execute_command( $res, $params );
-    return $ret ? 1 : 0;
-}
-
-=head2 maximize_window
-
- Description:
-    Maximizes the browser window
-
- Compatibility:
-    In webDriver 3 enabled selenium servers, you may only operate on the focused window.
-    As such, the window handle argument below will be ignored in this context.
-
-    Also, on chromedriver maximize is actually just setting the window size to the screen's
-    available height and width.
-
- Input:
-    STRING - <optional> - window handle (default is 'current' window)
-
- Output:
-    BOOLEAN - Success or failure
-
- Usage:
-    $driver->maximize_window();
-
-=cut
-
-sub maximize_window {
-    my ( $self, $window ) = @_;
-
-    $window = ( defined $window ) ? $window : 'current';
-    my $res = { 'command' => 'maximizeWindow', 'window_handle' => $window };
-    my $ret = $self->_execute_command($res);
-    return $ret ? 1 : 0;
-}
-
-=head2 minimize_window
-
- Description:
-    Minimizes the currently focused browser window (webdriver3 only)
-
- Output:
-    BOOLEAN - Success or failure
-
- Usage:
-    $driver->minimize_window();
-
-=cut
-
-sub minimize_window {
-    my ( $self, $window ) = @_;
-    $window = ( defined $window ) ? $window : 'current';
-    my $res = { 'command' => 'minimizeWindow', 'window_handle' => $window };
-    my $ret = $self->_execute_command($res);
-    return $ret ? 1 : 0;
-}
-
-=head2 fullscreen_window
-
- Description:
-    Fullscreens the currently focused browser window (webdriver3 only)
-
- Output:
-    BOOLEAN - Success or failure
-
- Usage:
-    $driver->fullscreen_window();
-
-=cut
-
-sub fullscreen_window {
-    my ( $self, $window ) = @_;
-    $window = ( defined $window ) ? $window : 'current';
-    my $res = { 'command' => 'fullscreenWindow', 'window_handle' => $window };
-    my $ret = $self->_execute_command($res);
-    return $ret ? 1 : 0;
-}
-
-=head2 get_all_cookies
-
- Description:
-    Retrieve all cookies visible to the current page. Each cookie will be
-    returned as a HASH reference with the following keys & their value types:
-
-    'name' - STRING
-    'value' - STRING
-    'path' - STRING
-    'domain' - STRING
-    'secure' - BOOLEAN
-
- Output:
-    ARRAY of HASHES - list of all the cookie hashes
-
- Usage:
-    print Dumper($driver->get_all_cookies());
-
-=cut
-
-sub get_all_cookies {
-    my ($self) = @_;
-    my $res = { 'command' => 'getAllCookies' };
-    return $self->_execute_command($res);
-}
-
-=head2 add_cookie
-
- Description:
-    Set a cookie on the domain.
-
- Input: 2 (4 optional)
-    Required:
-        'name'   - STRING
-        'value'  - STRING
-
-    Optional:
-        'path'   - STRING
-        'domain' - STRING
-        'secure'   - BOOLEAN - default false.
-        'httponly' - BOOLEAN - default false.
-        'expiry'   - TIME_T  - default 20 years in the future
-
- Usage:
-    $driver->add_cookie('foo', 'bar', '/', '.google.com', 0, 1)
-
-=cut
-
-sub add_cookie {
-    my ( $self, $name, $value, $path, $domain, $secure, $httponly, $expiry ) =
-      @_;
-
-    if (   ( not defined $name )
-        || ( not defined $value ) )
-    {
-        croak "Missing parameters";
-    }
-
-    my $res        = { 'command' => 'addCookie' };
-    my $json_false = JSON::false;
-    my $json_true  = JSON::true;
-    $secure = ( defined $secure && $secure ) ? $json_true : $json_false;
-
-    my $params = {
-        'cookie' => {
-            'name'   => $name,
-            'value'  => $value,
-            'path'   => $path,
-            'secure' => $secure,
-        }
-    };
-    $params->{cookie}->{domain}     = $domain   if $domain;
-    $params->{cookie}->{'httponly'} = $httponly if $httponly;
-    $params->{cookie}->{'expiry'}   = $expiry   if $expiry;
-
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 delete_all_cookies
-
- Description:
-    Delete all cookies visible to the current page.
-
- Usage:
-    $driver->delete_all_cookies();
-
-=cut
-
-sub delete_all_cookies {
-    my ($self) = @_;
-    my $res = { 'command' => 'deleteAllCookies' };
-    return $self->_execute_command($res);
-}
-
-=head2 get_cookie_named
-
-Basically get only the cookie with the provided name.
-Probably preferable to pick it out of the list unless you expect a *really* long list.
-
- Input:
-    Cookie Name - STRING
-
-Returns cookie definition hash, much like the elements in get_all_cookies();
-
-  Compatibility:
-    Only available on webdriver3 enabled selenium servers.
-
-=cut
-
-sub get_cookie_named {
-    my ( $self, $cookie_name ) = @_;
-    my $res = { 'command' => 'getCookieNamed', 'name' => $cookie_name };
-    return $self->_execute_command($res);
-}
-
-=head2 delete_cookie_named
-
- Description:
-    Delete the cookie with the given name. This command will be a no-op if there
-    is no such cookie visible to the current page.
-
- Input: 1
-    Required:
-        STRING - name of cookie to delete
-
- Usage:
-    $driver->delete_cookie_named('foo');
-
-=cut
-
-sub delete_cookie_named {
-    my ( $self, $cookie_name ) = @_;
-    if ( not defined $cookie_name ) {
-        croak "Cookie name not provided";
-    }
-    my $res = { 'command' => 'deleteCookieNamed', 'name' => $cookie_name };
-    return $self->_execute_command($res);
-}
-
-=head2 get_page_source
-
- Description:
-    Get the current page source.
-
- Output:
-    STRING - The page source.
-
- Usage:
-    print $driver->get_page_source();
-
-=cut
-
-sub get_page_source {
-    my ($self) = @_;
-    my $res = { 'command' => 'getPageSource' };
-    return $self->_execute_command($res);
-}
-
-=head2 find_element
-
- Description:
-    Search for an element on the page, starting from the document
-    root. The located element will be returned as a WebElement
-    object. If the element cannot be found, we will CROAK, killing
-    your script. If you wish for a warning instead, use the
-    parameterized version of the finders:
-
-        find_element_by_class
-        find_element_by_class_name
-        find_element_by_css
-        find_element_by_id
-        find_element_by_link
-        find_element_by_link_text
-        find_element_by_name
-        find_element_by_partial_link_text
-        find_element_by_tag_name
-        find_element_by_xpath
-
-    These functions all take a single STRING argument: the locator
-    search target of the element you want. If the element is found, we
-    will receive a WebElement. Otherwise, we will return 0. Note that
-    invoking methods on 0 will of course kill your script.
-
- Input: 2 (1 optional)
-    Required:
-        STRING - The search target.
-    Optional:
-        STRING - Locator scheme to use to search the element, available schemes:
-                 {class, class_name, css, id, link, link_text, partial_link_text,
-                  tag_name, name, xpath}
-                 Defaults to 'xpath' if not configured global during instantiation.
-
- Output:
-    Selenium::Remote::WebElement - WebElement Object
-        (This could be a subclass of L<Selenium::Remote::WebElement> if C<webelement_class> was set.
-
- Usage:
-    $driver->find_element("//input[\@name='q']");
-
-=cut
-
-sub find_element {
-    my ( $self, $query, $method ) = @_;
-    if ( not defined $query ) {
-        croak 'Search string to find element not provided.';
-    }
-
-    my $res = { 'command' => 'findElement' };
-    my $params = $self->_build_find_params( $method, $query );
-    my $ret_data = eval { $self->_execute_command( $res, $params ); };
-    if ($@) {
-        if ( $@ =~
-/(An element could not be located on the page using the given search parameters)/
-          )
-        {
-            # give details on what element wasn't found
-            $@ = "$1: $query,$params->{using}";
-            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
-            croak $@;
-        }
-        else {
-            # re throw if the exception wasn't what we expected
-            die $@;
-        }
-    }
-    return $self->webelement_class->new(
-        id     => $ret_data,
-        driver => $self
-    );
-}
-
-=head2 find_elements
-
- Description:
-    Search for multiple elements on the page, starting from the document root.
-    The located elements will be returned as an array of WebElement object.
-
- Input: 2 (1 optional)
-    Required:
-        STRING - The search target.
-    Optional:
-        STRING - Locator scheme to use to search the element, available schemes:
-                 {class, class_name, css, id, link, link_text, partial_link_text,
-                  tag_name, name, xpath}
-                 Defaults to 'xpath' if not configured global during instantiation.
-
- Output:
-    ARRAY or ARRAYREF of WebElement Objects
-
- Usage:
-    $driver->find_elements("//input");
-
-=cut
-
-sub find_elements {
-    my ( $self, $query, $method ) = @_;
-    if ( not defined $query ) {
-        croak 'Search string to find element not provided.';
-    }
-
-    my $res = { 'command' => 'findElements' };
-    my $params = $self->_build_find_params( $method, $query );
-    my $ret_data = eval { $self->_execute_command( $res, $params ); };
-    if ($@) {
-        if ( $@ =~
-/(An element could not be located on the page using the given search parameters)/
-          )
-        {
-            # give details on what element wasn't found
-            $@ = "$1: $query,$params->{using}";
-            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
-            croak $@;
-        }
-        else {
-            # re throw if the exception wasn't what we expected
-            die $@;
-        }
-    }
-    my $elem_obj_arr = [];
-    foreach (@$ret_data) {
-        push(
-            @$elem_obj_arr,
-            $self->webelement_class->new(
-                id     => $_,
-                driver => $self
-            )
-        );
-    }
-    return wantarray ? @{$elem_obj_arr} : $elem_obj_arr;
-}
-
-=head2 find_child_element
-
- Description:
-    Search for an element on the page, starting from the identified element. The
-    located element will be returned as a WebElement object.
-
- Input: 3 (1 optional)
-    Required:
-        Selenium::Remote::WebElement - WebElement object from where you want to
-                                       start searching.
-        STRING - The search target. (Do not use a double whack('//')
-                 in an xpath to search for a child element
-                 ex: '//option[@id="something"]'
-                 instead use a dot whack ('./')
-                 ex: './option[@id="something"]')
-    Optional:
-        STRING - Locator scheme to use to search the element, available schemes:
-                 {class, class_name, css, id, link, link_text, partial_link_text,
-                  tag_name, name, xpath}
-                 Defaults to 'xpath' if not configured global during instantiation.
-
- Output:
-    WebElement Object
-
- Usage:
-    my $elem1 = $driver->find_element("//select[\@name='ned']");
-    # note the usage of ./ when searching for a child element instead of //
-    my $child = $driver->find_child_element($elem1, "./option[\@value='es_ar']");
-
-=cut
-
-sub find_child_element {
-    my ( $self, $elem, $query, $method ) = @_;
-    if ( ( not defined $elem ) || ( not defined $query ) ) {
-        croak "Missing parameters";
-    }
-    my $res = { 'command' => 'findChildElement', 'id' => $elem->{id} };
-    my $params = $self->_build_find_params( $method, $query );
-    my $ret_data = eval { $self->_execute_command( $res, $params ); };
-    if ($@) {
-        if ( $@ =~
-/(An element could not be located on the page using the given search parameters)/
-          )
-        {
-            # give details on what element wasn't found
-            $@ = "$1: $query,$params->{using}";
-            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
-            croak $@;
-        }
-        else {
-            # re throw if the exception wasn't what we expected
-            die $@;
-        }
-    }
-    return $self->webelement_class->new(
-        id     => $ret_data,
-        driver => $self
-    );
-}
-
-=head2 find_child_elements
-
- Description:
-    Search for multiple element on the page, starting from the identified
-    element. The located elements will be returned as an array of WebElement
-    objects.
-
- Input: 3 (1 optional)
-    Required:
-        Selenium::Remote::WebElement - WebElement object from where you want to
-                                       start searching.
-        STRING - The search target.
-    Optional:
-        STRING - Locator scheme to use to search the element, available schemes:
-                 {class, class_name, css, id, link, link_text, partial_link_text,
-                  tag_name, name, xpath}
-                 Defaults to 'xpath' if not configured global during instantiation.
-
- Output:
-    ARRAY of WebElement Objects.
-
- Usage:
-    my $elem1 = $driver->find_element("//select[\@name='ned']");
-    # note the usage of ./ when searching for a child element instead of //
-    my $child = $driver->find_child_elements($elem1, "./option");
-
-=cut
-
-sub find_child_elements {
-    my ( $self, $elem, $query, $method ) = @_;
-    if ( ( not defined $elem ) || ( not defined $query ) ) {
-        croak "Missing parameters";
-    }
-
-    my $res = { 'command' => 'findChildElements', 'id' => $elem->{id} };
-    my $params = $self->_build_find_params( $method, $query );
-    my $ret_data = eval { $self->_execute_command( $res, $params ); };
-    if ($@) {
-        if ( $@ =~
-/(An element could not be located on the page using the given search parameters)/
-          )
-        {
-            # give details on what element wasn't found
-            $@ = "$1: $query,$params->{using}";
-            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
-            croak $@;
-        }
-        else {
-            # re throw if the exception wasn't what we expected
-            die $@;
-        }
-    }
-    my $elem_obj_arr = [];
-    my $i            = 0;
-    foreach (@$ret_data) {
-        $elem_obj_arr->[$i] = $self->webelement_class->new(
-            id     => $_,
-            driver => $self
-        );
-        $i++;
-    }
-    return wantarray ? @{$elem_obj_arr} : $elem_obj_arr;
-}
-
-=head2 find_element_by_class
-
-See L</find_element>.
-
-=head2 find_element_by_class_name
-
-See L</find_element>.
-
-=head2 find_element_by_css
-
-See L</find_element>.
-
-=head2 find_element_by_id
-
-See L</find_element>.
-
-=head2 find_element_by_link
-
-See L</find_element>.
-
-=head2 find_element_by_link_text
-
-See L</find_element>.
-
-=head2 find_element_by_name
-
-See L</find_element>.
-
-=head2 find_element_by_partial_link_text
-
-See L</find_element>.
-
-=head2 find_element_by_tag_name
-
-See L</find_element>.
-
-=head2 find_element_by_xpath
-
-See L</find_element>.
-
-=head2 get_active_element
-
- Description:
-    Get the element on the page that currently has focus.. The located element
-    will be returned as a WebElement object.
-
- Output:
-    WebElement Object
-
- Usage:
-    $driver->get_active_element();
-
-=cut
-
-sub _build_find_params {
-    my ( $self, $method, $query ) = @_;
-
-    my $using = $self->_build_using($method);
-
-    # geckodriver doesn't accept name as a valid selector
-    if ( $self->isa('Selenium::Firefox') && $using eq 'name' ) {
-        return {
-            using => 'css selector',
-            value => qq{[name="$query"]}
-        };
-    }
-    else {
-        return {
-            using => $using,
-            value => $query
-        };
-    }
-}
-
-sub _build_using {
-    my ( $self, $method ) = @_;
-
-    if ($method) {
-        if ( $self->FINDERS->{$method} ) {
-            return $self->FINDERS->{$method};
-        }
-        else {
-            croak 'Bad method, expected: '
-              . join( ', ', keys %{ $self->FINDERS } )
-              . ", got $method";
-        }
-    }
-    else {
-        return $self->default_finder;
-    }
-}
-
-sub get_active_element {
-    my ($self) = @_;
-    my $res = { 'command' => 'getActiveElement' };
-    my $ret_data = eval { $self->_execute_command($res) };
-    if ($@) {
-        croak $@;
-    }
-    else {
-        return $self->webelement_class->new(
-            id     => $ret_data,
-            driver => $self
-        );
-    }
-}
-
-=head2 cache_status
-
- Description:
-    Get the status of the html5 application cache.
-
- Usage:
-    print $driver->cache_status;
-
- Output:
-    <number> - Status code for application cache: {UNCACHED = 0, IDLE = 1, CHECKING = 2, DOWNLOADING = 3, UPDATE_READY = 4, OBSOLETE = 5}
-
-=cut
-
-sub cache_status {
-    my ($self) = @_;
-    my $res = { 'command' => 'cacheStatus' };
-    return $self->_execute_command($res);
-}
-
-=head2 set_geolocation
-
- Description:
-    Set the current geographic location - note that your driver must
-    implement this endpoint, or else it will crash your session. At the
-    very least, it works in v2.12 of Chromedriver.
-
- Input:
-    Required:
-        HASH: A hash with key C<location> whose value is a Location hashref. See
-        usage section for example.
-
- Usage:
-    $driver->set_geolocation( location => {
-        latitude  => 40.714353,
-        longitude => -74.005973,
-        altitude  => 0.056747
-    });
-
- Output:
-    BOOLEAN - success or failure
-
-=cut
-
-sub set_geolocation {
-    my ( $self, %params ) = @_;
-    my $res = { 'command' => 'setGeolocation' };
-    return $self->_execute_command( $res, \%params );
-}
-
-=head2 get_geolocation
-
- Description:
-    Get the current geographic location. Note that your webdriver must
-    implement this endpoint - otherwise, it will crash your session. At
-    the time of release, we couldn't get this to work on the desktop
-    FirefoxDriver or desktop Chromedriver.
-
- Usage:
-    print $driver->get_geolocation;
-
- Output:
-    { latitude: number, longitude: number, altitude: number } - The current geo location.
-
-=cut
-
-sub get_geolocation {
-    my ($self) = @_;
-    my $res = { 'command' => 'getGeolocation' };
-    return $self->_execute_command($res);
-}
-
-=head2 get_log
-
- Description:
-    Get the log for a given log type. Log buffer is reset after each request.
-
- Input:
-    Required:
-        <STRING> - Type of log to retrieve:
-        {client|driver|browser|server}. There may be others available; see
-        get_log_types for a full list for your driver.
-
- Usage:
-    $driver->get_log( $log_type );
-
- Output:
-    <ARRAY|ARRAYREF> - An array of log entries since the most recent request.
-
-=cut
-
-sub get_log {
-    my ( $self, $type ) = @_;
-    my $res = { 'command' => 'getLog' };
-    return $self->_execute_command( $res, { type => $type } );
-}
-
-=head2 get_log_types
-
- Description:
-    Get available log types. By default, every driver should have client,
-    driver, browser, and server types, but there may be more available,
-    depending on your driver.
-
- Usage:
-    my @types = $driver->get_log_types;
-    $driver->get_log($types[0]);
-
- Output:
-    <ARRAYREF> - The list of log types.
-
-=cut
-
-sub get_log_types {
-    my ($self) = @_;
-    my $res = { 'command' => 'getLogTypes' };
-    return $self->_execute_command($res);
-}
-
-=head2 set_orientation
-
- Description:
-    Set the browser orientation.
-
- Input:
-    Required:
-        <STRING> - Orientation {LANDSCAPE|PORTRAIT}
-
- Usage:
-    $driver->set_orientation( $orientation  );
-
- Output:
-    BOOLEAN - success or failure
-
-=cut
-
-sub set_orientation {
-    my ( $self, $orientation ) = @_;
-    my $res = { 'command' => 'setOrientation' };
-    return $self->_execute_command( $res, { orientation => $orientation } );
-}
-
-=head2 get_orientation
-
- Description:
-    Get the current browser orientation. Returns either LANDSCAPE|PORTRAIT.
-
- Usage:
-    print $driver->get_orientation;
-
- Output:
-    <STRING> - your orientation.
-
-=cut
-
-sub get_orientation {
-    my ($self) = @_;
-    my $res = { 'command' => 'getOrientation' };
-    return $self->_execute_command($res);
-}
-
-=head2 send_modifier
-
- Description:
-    Send an event to the active element to depress or release a modifier key.
-
- Input: 2
-    Required:
-      value - String - The modifier key event to be sent. This key must be one 'Ctrl','Shift','Alt',' or 'Command'/'Meta' as defined by the send keys command
-      isdown - Boolean/String - Whether to generate a key down or key up
-
- Usage:
-    $driver->send_modifier('Alt','down');
-    $elem->send_keys('c');
-    $driver->send_modifier('Alt','up');
-
-    or
-
-    $driver->send_modifier('Alt',1);
-    $elem->send_keys('c');
-    $driver->send_modifier('Alt',0);
-
-=cut
-
-sub send_modifier {
-    my ( $self, $modifier, $isdown ) = @_;
-    if ( $isdown =~ /(down|up)/ ) {
-        $isdown = $isdown =~ /down/ ? 1 : 0;
-    }
-
-    if ( $self->{is_wd3}
-        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
-    {
-        my $acts = [
-            {
-                type => $isdown ? 'keyDown' : 'keyUp',
-                value => KEYS->{ lc($modifier) },
-            },
-        ];
-
-        my $action = {
-            actions => [
-                {
-                    id      => 'key',
-                    type    => 'key',
-                    actions => $acts,
-                }
-            ]
-        };
-        _queue_action(%$action);
-        return 1;
-    }
-
-    my $res = { 'command' => 'sendModifier' };
-    my $params = {
-        value  => $modifier,
-        isdown => $isdown
-    };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 compare_elements
-
- Description:
-    Test if two element IDs refer to the same DOM element.
-
- Input: 2
-    Required:
-        Selenium::Remote::WebElement - WebElement Object
-        Selenium::Remote::WebElement - WebElement Object
-
- Output:
-    BOOLEAN
-
- Usage:
-    $driver->compare_elements($elem_obj1, $elem_obj2);
-
-=cut
-
-sub compare_elements {
-    my ( $self, $elem1, $elem2 ) = @_;
-    my $res = {
-        'command' => 'elementEquals',
-        'id'      => $elem1->{id},
-        'other'   => $elem2->{id}
-    };
-    return $self->_execute_command($res);
-}
-
-=head2 click
-
- Description:
-    Click any mouse button (at the coordinates set by the last moveto command).
-
- Input:
-    button - any one of 'LEFT'/0 'MIDDLE'/1 'RIGHT'/2
-             defaults to 'LEFT'
-    queue - (optional) queue the click, rather than executing it.  WD3 only.
-
- Usage:
-    $driver->click('LEFT');
-    $driver->click(1); #MIDDLE
-    $driver->click('RIGHT');
-    $driver->click;  #Defaults to left
-
-=cut
-
-sub click {
-    my ( $self, $button, $append ) = @_;
-    $button = _get_button($button);
-
-    my $res    = { 'command' => 'click' };
-    my $params = { 'button'  => $button };
-
-    if ( $self->{is_wd3}
-        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
-    {
-        $params = {
-            actions => [
-                {
-                    type       => "pointer",
-                    id         => 'mouse',
-                    parameters => { "pointerType" => "mouse" },
-                    actions    => [
-                        {
-                            type     => "pointerDown",
-                            duration => 0,
-                            button   => $button,
-                        },
-                        {
-                            type     => "pointerUp",
-                            duration => 0,
-                            button   => $button,
-                        },
-                    ],
-                }
-            ],
-        };
-        if ($append) {
-            _queue_action(%$params);
-            return 1;
-        }
-        return $self->general_action(%$params);
-    }
-
-    return $self->_execute_command( $res, $params );
-}
-
-sub _get_button {
-    my $button = shift;
-    my $button_enum = { LEFT => 0, MIDDLE => 1, RIGHT => 2 };
-    if ( defined $button && $button =~ /(LEFT|MIDDLE|RIGHT)/i ) {
-        return $button_enum->{ uc $1 };
-    }
-    if ( defined $button && $button =~ /(0|1|2)/ ) {
-        #Handle user error sending in "1"
-        return int($1);
-    }
-    return 0;
-}
-
-=head2 double_click
-
- Description:
-    Double-clicks at the current mouse coordinates (set by moveto).
-
- Compatibility:
-    On webdriver3 enabled servers, you can double click arbitrary mouse buttons.
-
- Usage:
-    $driver->double_click(button);
-
-=cut
-
-sub double_click {
-    my ( $self, $button ) = @_;
-
-    $button = _get_button($button);
-
-    if ( $self->{is_wd3}
-        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
-    {
-        $self->click( $button, 1 );
-        $self->click( $button, 1 );
-        return $self->general_action();
-    }
-
-    my $res = { 'command' => 'doubleClick' };
-    return $self->_execute_command($res);
-}
-
-=head2 button_down
-
- Description:
-    Click and hold the left mouse button (at the coordinates set by the
-    last moveto command). Note that the next mouse-related command that
-    should follow is buttonup . Any other mouse command (such as click
-    or another call to buttondown) will yield undefined behaviour.
-
- Compatibility:
-    On WebDriver 3 enabled servers, all this does is queue a button down action.
-    You will either have to call general_action() to perform the queue, or an action like click() which also clears the queue.
-
- Usage:
-    $self->button_down;
-
-=cut
-
-sub button_down {
-    my ($self) = @_;
-
-    if ( $self->{is_wd3}
-        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
-    {
-        my $params = {
-            actions => [
-                {
-                    type       => "pointer",
-                    id         => 'mouse',
-                    parameters => { "pointerType" => "mouse" },
-                    actions    => [
-                        {
-                            type     => "pointerDown",
-                            duration => 0,
-                            button   => 0,
-                        },
-                    ],
-                }
-            ],
-        };
-        _queue_action(%$params);
-        return 1;
-    }
-
-    my $res = { 'command' => 'buttonDown' };
-    return $self->_execute_command($res);
-}
-
-=head2 button_up
-
- Description:
-    Releases the mouse button previously held (where the mouse is
-    currently at). Must be called once for every buttondown command
-    issued. See the note in click and buttondown about implications of
-    out-of-order commands.
-
- Compatibility:
-    On WebDriver 3 enabled servers, all this does is queue a button down action.
-    You will either have to call general_action() to perform the queue, or an action like click() which also clears the queue.
-
- Usage:
-    $self->button_up;
-
-=cut
-
-sub button_up {
-    my ($self) = @_;
-
-    if ( $self->{is_wd3}
-        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
-    {
-        my $params = {
-            actions => [
-                {
-                    type       => "pointer",
-                    id         => 'mouse',
-                    parameters => { "pointerType" => "mouse" },
-                    actions    => [
-                        {
-                            type     => "pointerDown",
-                            duration => 0,
-                            button   => 0,
-                        },
-                    ],
-                }
-            ],
-        };
-        _queue_action(%$params);
-        return 1;
-    }
-
-    my $res = { 'command' => 'buttonUp' };
-    return $self->_execute_command($res);
-}
-
-=head2 upload_file
-
- Description:
-    Upload a file from the local machine to the selenium server
-    machine. That file then can be used for testing file upload on web
-    forms. Returns the remote-server's path to the file.
-
-    Passing raw data as an argument past the filename will upload
-    that rather than the file's contents.
-
-    When passing raw data, be advised that it expects a zipped
-    and then base64 encoded version of a single file.
-    Multiple files and/or directories are not supported by the remote server.
-
- Usage:
-    my $remote_fname = $driver->upload_file( $fname );
-    my $element = $driver->find_element( '//input[@id="file"]' );
-    $element->send_keys( $remote_fname );
-
-=cut
-
-# this method duplicates upload() method in the
-# org.openqa.selenium.remote.RemoteWebElement java class.
-
-sub upload_file {
-    my ( $self, $filename, $raw_content ) = @_;
-
-    my $params;
-    if ( defined $raw_content ) {
-
-        #If no processing is passed, send the argument raw
-        $params = { file => $raw_content };
-    }
-    else {
-        #Otherwise, zip/base64 it.
-        $params = $self->_prepare_file($filename);
-    }
-
-    my $res = { 'command' => 'uploadFile' };    # /session/:SessionId/file
-    my $ret = $self->_execute_command( $res, $params );
-
-    return $ret;
-}
-
-sub _prepare_file {
-    my ( $self, $filename ) = @_;
-
-    if ( not -r $filename ) { croak "upload_file: no such file: $filename"; }
-    my $string = "";                            # buffer
-    my $zip    = Archive::Zip->new();
-    $zip->addFile( $filename, basename($filename) );
-    if ( $zip->writeToFileHandle( IO::String->new($string) ) != AZ_OK ) {
-        die 'zip failed';
-    }
-
-    return { file => MIME::Base64::encode_base64( $string, '' ) };
-}
-
-=head2 get_text
-
- Description:
-    Get the text of a particular element. Wrapper around L</find_element>
-
- Usage:
-    $text = $driver->get_text("//div[\@name='q']");
-
-=cut
-
-sub get_text {
-    my $self = shift;
-    return $self->find_element(@_)->get_text();
-}
-
-=head2 get_body
-
- Description:
-    Get the current text for the whole body. If you want the entire raw HTML instead,
-    See L</get_page_source>.
-
- Usage:
-    $body_text = $driver->get_body();
-
-=cut
-
-sub get_body {
-    my $self = shift;
-    return $self->get_text( '//body', 'xpath' );
-}
-
-=head2 get_path
-
- Description:
-     Get the path part of the current browser location.
-
- Usage:
-     $path = $driver->get_path();
-
-=cut
-
-sub get_path {
-    my $self     = shift;
-    my $location = $self->get_current_url;
-    $location =~ s/\?.*//;               # strip of query params
-    $location =~ s/#.*//;                # strip of anchors
-    $location =~ s#^https?://[^/]+##;    # strip off host
-    return $location;
-}
-
-=head2 get_user_agent
-
- Description:
-    Convenience method to get the user agent string, according to the
-    browser's value for window.navigator.userAgent.
-
- Usage:
-    $user_agent = $driver->get_user_agent()
-
-=cut
-
-sub get_user_agent {
-    my $self = shift;
-    return $self->execute_script('return window.navigator.userAgent;');
-}
-
-=head2 set_inner_window_size
-
- Description:
-     Set the inner window size by closing the current window and
-     reopening the current page in a new window. This can be useful
-     when using browsers to mock as mobile devices.
-
-     This sub will be fired automatically if you set the
-     C<inner_window_size> hash key option during instantiation.
-
- Input:
-     INT - height of the window
-     INT - width of the window
-
- Output:
-     BOOLEAN - Success or failure
-
- Usage:
-     $driver->set_inner_window_size(640, 480)
-
-=cut
-
-sub set_inner_window_size {
-    my $self     = shift;
-    my $height   = shift;
-    my $width    = shift;
-    my $location = $self->get_current_url;
-
-    $self->execute_script( 'window.open("' . $location . '", "_blank")' );
-    $self->close;
-    my @handles = @{ $self->get_window_handles };
-    $self->switch_to_window( pop @handles );
-
-    my @resize = (
-        'window.innerHeight = ' . $height,
-        'window.innerWidth  = ' . $width,
-        'return 1'
-    );
-
-    return $self->execute_script( join( ';', @resize ) ) ? 1 : 0;
-}
-
-=head2 get_local_storage_item
-
- Description:
-     Get the value of a local storage item specified by the given key.
-
- Input: 1
-    Required:
-        STRING - name of the key to be retrieved
-
- Output:
-     STRING - value of the local storage item
-
- Usage:
-     $driver->get_local_storage_item('key')
-
-=cut
-
-sub get_local_storage_item {
-    my ( $self, $key ) = @_;
-    my $res    = { 'command' => 'getLocalStorageItem' };
-    my $params = { 'key'     => $key };
-    return $self->_execute_command( $res, $params );
-}
-
-=head2 delete_local_storage_item
-
- Description:
-     Get the value of a local storage item specified by the given key.
-
- Input: 1
-    Required
-        STRING - name of the key to be deleted
-
- Usage:
-     $driver->delete_local_storage_item('key')
-
-=cut
-
-sub delete_local_storage_item {
-    my ( $self, $key ) = @_;
-    my $res    = { 'command' => 'deleteLocalStorageItem' };
-    my $params = { 'key'     => $key };
-    return $self->_execute_command( $res, $params );
-}
-
-sub _coerce_timeout_ms {
-    my ($ms) = @_;
-
-    if ( defined $ms ) {
-        return _coerce_number($ms);
-    }
-    else {
-        croak 'Expecting a timeout in ms';
-    }
-}
-
-sub _coerce_number {
-    my ($maybe_number) = @_;
-
-    if ( Scalar::Util::looks_like_number($maybe_number) ) {
-        return $maybe_number + 0;
-    }
-    else {
-        croak "Expecting a number, not: $maybe_number";
-    }
+sub new {
+	my ( $class, %opts ) = @_;
+	my $conn = Selenium::Remote::RemoteConnection->new(
+		remote_server_addr => $opts{remote_server_addr},
+		port               => $opts{port},
+	);
+	$conn->check_status();
+	if( $conn->{'version'} && $conn->{'version'} == 4 ) {
+		require Selenium::Remote::Driver::v4;
+		return Selenium::Remote::Driver::v4->new(
+			'port'    => $opts{port},
+			'host'    => $opts{remote_server_addr},
+			'browser' => $opts{browser_name},
+			'debug'   => $opts{debug},
+		);
+	}
+	require Selenium::Remote::Driver::v3;
+	return Selenium::Remote::Driver::v3->new(%opts);
 }
 
 1;

+ 3257 - 0
lib/Selenium/Remote/Driver/v3.pm

@@ -0,0 +1,3257 @@
+package Selenium::Remote::Driver::v3;
+
+use strict;
+use warnings;
+
+# ABSTRACT: Perl Client for Selenium Remote Driver
+
+use Moo;
+use Try::Tiny;
+
+use 5.006;
+use v5.10.0;    # Before 5.006, v5.10.0 would not be understood.
+
+# See http://perldoc.perl.org/5.10.0/functions/use.html#use-VERSION
+# and http://www.dagolden.com/index.php/369/version-numbers-should-be-boring/
+# for details.
+
+use Carp;
+our @CARP_NOT;
+
+use IO::String;
+use Archive::Zip qw( :ERROR_CODES );
+use Scalar::Util;
+use Selenium::Remote::RemoteConnection;
+use Selenium::Remote::Commands;
+use Selenium::Remote::Spec;
+use Selenium::Remote::WebElement;
+use Selenium::Remote::WDKeys;
+use File::Spec::Functions ();
+use File::Basename qw(basename);
+use Sub::Install ();
+use MIME::Base64 ();
+use Time::HiRes qw(usleep);
+use Clone qw{clone};
+use List::Util qw{any};
+
+use constant FINDERS => {
+    class             => 'class name',
+    class_name        => 'class name',
+    css               => 'css selector',
+    id                => 'id',
+    link              => 'link text',
+    link_text         => 'link text',
+    name              => 'name',
+    partial_link_text => 'partial link text',
+    tag_name          => 'tag name',
+    xpath             => 'xpath',
+};
+
+our $FORCE_WD2            = 0;
+our $FORCE_WD3            = 0;
+our $FORCE_WD4            = 0;
+our %CURRENT_ACTION_CHAIN = ( actions => [] );
+
+has 'remote_server_addr' => (
+    is     => 'rw',
+    coerce => sub { ( defined( $_[0] ) ? $_[0] : 'localhost' ) },
+    default   => sub { 'localhost' },
+    predicate => 1
+);
+
+has 'browser_name' => (
+    is     => 'rw',
+    coerce => sub { ( defined( $_[0] ) ? $_[0] : 'firefox' ) },
+    default => sub { 'firefox' },
+);
+
+has 'base_url' => (
+    is     => 'lazy',
+    coerce => sub {
+        my $base_url = shift;
+        $base_url =~ s|/$||;
+        return $base_url;
+    },
+    predicate => 'has_base_url',
+);
+
+has 'platform' => (
+    is     => 'rw',
+    coerce => sub { ( defined( $_[0] ) ? $_[0] : 'ANY' ) },
+    default => sub { 'ANY' },
+);
+
+has 'port' => (
+    is     => 'rw',
+    coerce => sub { ( defined( $_[0] ) ? $_[0] : '4444' ) },
+    default   => sub { '4444' },
+    predicate => 1
+);
+
+has 'version' => (
+    is      => 'rw',
+    default => sub { '' },
+);
+
+has 'webelement_class' => (
+    is      => 'rw',
+    default => sub { 'Selenium::Remote::WebElement' },
+);
+
+has 'default_finder' => (
+    is      => 'rw',
+    coerce  => sub { __PACKAGE__->FINDERS->{ $_[0] } },
+    default => sub { 'xpath' },
+);
+
+has 'session_id' => (
+    is      => 'rw',
+    default => sub { undef },
+);
+
+has 'remote_conn' => (
+    is      => 'lazy',
+    builder => sub {
+        my $self = shift;
+        return Selenium::Remote::RemoteConnection->new(
+            remote_server_addr => $self->remote_server_addr,
+            port               => $self->port,
+            ua                 => $self->ua,
+            wd_context_prefix  => $self->wd_context_prefix
+        );
+    },
+);
+
+has 'error_handler' => (
+    is     => 'rw',
+    coerce => sub {
+        my ($maybe_coderef) = @_;
+
+        if ( ref($maybe_coderef) eq 'CODE' ) {
+            return $maybe_coderef;
+        }
+        else {
+            croak 'The error handler must be a code ref.';
+        }
+    },
+    clearer   => 1,
+    predicate => 1
+);
+
+has 'ua' => (
+    is      => 'lazy',
+    builder => sub { return LWP::UserAgent->new }
+);
+
+has 'commands' => (
+    is      => 'lazy',
+    builder => sub {
+        return Selenium::Remote::Commands->new;
+    },
+);
+
+has 'commands_v3' => (
+    is      => 'lazy',
+    builder => sub {
+        return Selenium::Remote::Spec->new;
+    },
+);
+
+has 'auto_close' => (
+    is     => 'rw',
+    coerce => sub { ( defined( $_[0] ) ? $_[0] : 1 ) },
+    default => sub { 1 },
+);
+
+has 'pid' => (
+    is      => 'lazy',
+    builder => sub { return $$ }
+);
+
+has 'javascript' => (
+    is     => 'rw',
+    coerce => sub { $_[0] ? JSON::true : JSON::false },
+    default => sub { return JSON::true }
+);
+
+has 'accept_ssl_certs' => (
+    is     => 'rw',
+    coerce => sub { $_[0] ? JSON::true : JSON::false },
+    default => sub { return JSON::true }
+);
+
+has 'proxy' => (
+    is     => 'rw',
+    coerce => sub {
+        my $proxy = $_[0];
+        if ( $proxy->{proxyType} =~ /^pac$/i ) {
+            if ( not defined $proxy->{proxyAutoconfigUrl} ) {
+                croak "proxyAutoconfigUrl not provided\n";
+            }
+            elsif ( not( $proxy->{proxyAutoconfigUrl} =~ /^(http|file)/g ) ) {
+                croak
+                  "proxyAutoconfigUrl should be of format http:// or file://";
+            }
+
+            if ( $proxy->{proxyAutoconfigUrl} =~ /^file/ ) {
+                my $pac_url = $proxy->{proxyAutoconfigUrl};
+                my $file    = $pac_url;
+                $file =~ s{^file://}{};
+
+                if ( !-e $file ) {
+                    warn "proxyAutoConfigUrl file does not exist: '$pac_url'";
+                }
+            }
+        }
+        $proxy;
+    },
+);
+
+has 'extra_capabilities' => (
+    is      => 'rw',
+    default => sub { {} }
+);
+
+has 'firefox_profile' => (
+    is     => 'rw',
+    coerce => sub {
+        my $profile = shift;
+        unless ( Scalar::Util::blessed($profile)
+            && $profile->isa('Selenium::Firefox::Profile') )
+        {
+            croak "firefox_profile should be a Selenium::Firefox::Profile\n";
+        }
+
+        return $profile;
+    },
+    predicate => 'has_firefox_profile',
+    clearer   => 1
+);
+
+has debug => (
+    is => 'lazy',
+    default => sub { 0 },
+);
+
+has 'desired_capabilities' => (
+    is        => 'lazy',
+    predicate => 'has_desired_capabilities'
+);
+
+has 'inner_window_size' => (
+    is        => 'lazy',
+    predicate => 1,
+    coerce    => sub {
+        my $size = shift;
+
+        croak "inner_window_size must have two elements: [ height, width ]"
+          unless scalar @$size == 2;
+
+        foreach my $dim (@$size) {
+            croak 'inner_window_size only accepts integers, not: ' . $dim
+              unless Scalar::Util::looks_like_number($dim);
+        }
+
+        return $size;
+    },
+
+);
+
+# At the time of writing, Geckodriver uses a different endpoint than
+# the java bindings for executing synchronous and asynchronous
+# scripts. As a matter of fact, Geckodriver does conform to the W3C
+# spec, but as are bound to support both while the java bindings
+# transition to full spec support, we need some way to handle the
+# difference.
+
+has '_execute_script_suffix' => (
+    is      => 'lazy',
+    default => ''
+);
+
+with 'Selenium::Remote::Finders';
+with 'Selenium::Remote::Driver::CanSetWebdriverContext';
+
+sub BUILD {
+    my $self = shift;
+
+    if ( !( defined $self->session_id ) ) {
+        if ( $self->has_desired_capabilities ) {
+            $self->new_desired_session( $self->desired_capabilities );
+        }
+        else {
+            # Connect to remote server & establish a new session
+            $self->new_session( $self->extra_capabilities );
+        }
+    }
+
+    if ( !( defined $self->session_id ) ) {
+        croak "Could not establish a session with the remote server\n";
+    }
+    elsif ( $self->has_inner_window_size ) {
+        my $size = $self->inner_window_size;
+        $self->set_inner_window_size(@$size);
+    }
+
+    #Set debug if needed
+    $self->debug_on() if $self->debug;
+
+    # Setup non-croaking, parameter versions of finders
+    foreach my $by ( keys %{ $self->FINDERS } ) {
+        my $finder_name = 'find_element_by_' . $by;
+
+        # In case we get instantiated multiple times, we don't want to
+        # install into the name space every time.
+        unless ( $self->can($finder_name) ) {
+            my $find_sub = $self->_build_find_by($by);
+
+            Sub::Install::install_sub(
+                {
+                    code => $find_sub,
+                    into => __PACKAGE__,
+                    as   => $finder_name,
+                }
+            );
+        }
+    }
+}
+
+sub new_from_caps {
+    my ( $self, %args ) = @_;
+
+    if ( not exists $args{desired_capabilities} ) {
+        $args{desired_capabilities} = {};
+    }
+
+    return $self->new(%args);
+}
+
+sub DEMOLISH {
+    my ( $self, $in_global_destruction ) = @_;
+    return if $$ != $self->pid;
+    return if $in_global_destruction;
+    $self->quit() if ( $self->auto_close && defined $self->session_id );
+}
+
+# We install an 'around' because we can catch more exceptions this way
+# than simply wrapping the explicit croaks in _execute_command.
+# @args should be fed to the handler to provide context
+# return_value could be assigned from the handler if we want to allow the
+# error_handler to handle the errors
+
+around '_execute_command' => sub {
+    my $orig = shift;
+    my $self = shift;
+
+    # copy @_ because it gets lost in the way
+    my @args = @_;
+    my $return_value;
+    try {
+        $return_value = $orig->( $self, @args );
+    }
+    catch {
+        if ( $self->has_error_handler ) {
+            $return_value = $self->error_handler->( $self, $_, @args );
+        }
+        else {
+            croak $_;
+        }
+    };
+    return $return_value;
+};
+
+sub _get_resource {
+    my ( $self, $res ) = @_;
+    return $self->commands_v3->get_params($res) if $self->{is_wd3};
+    return $self->commands->get_params($res);
+
+}
+
+# This is an internal method used the Driver & is not supposed to be used by
+# end user. This method is used by Driver to set up all the parameters
+# (url & JSON), send commands & receive processed response from the server.
+sub _execute_command {
+    my ( $self, $res, $params ) = @_;
+    $res->{'session_id'} = $self->session_id;
+
+    print "Prepping $res->{command}\n" if $self->{debug};
+
+    #webdriver 3 shims
+    return $self->{capabilities}
+      if $res->{command} eq 'getCapabilities' && $self->{capabilities};
+    $res->{ms}    = $params->{ms}    if $params->{ms};
+    $res->{type}  = $params->{type}  if $params->{type};
+    $res->{text}  = $params->{text}  if $params->{text};
+    $res->{using} = $params->{using} if $params->{using};
+    $res->{value} = $params->{value} if $params->{value};
+
+    print "Executing $res->{command}\n" if $self->{debug};
+    my $resource = $self->_get_resource($res);
+
+    #Fall-back to legacy if wd3 command doesn't exist
+    if ( !$resource && $self->{is_wd3} ) {
+        print "Falling back to legacy selenium method for $res->{command}\n"
+          if $self->{debug};
+        $resource = $self->commands->get_params($res);
+    }
+
+    #XXX InternetExplorerDriver quirks
+    if ( $self->{is_wd3} && $self->browser_name eq 'internet explorer' ) {
+        delete $params->{ms};
+        delete $params->{type};
+        delete $resource->{payload}->{type};
+        my $oldvalue = delete $params->{'page load'};
+        $params->{pageLoad} = $oldvalue if $oldvalue;
+    }
+
+    if ($resource) {
+        $params = {} unless $params;
+        my $resp = $self->remote_conn->request( $resource, $params );
+
+#In general, the parse_response for v3 is better, which is why we use it *even if* we are falling back.
+        return $self->commands_v3->parse_response( $res, $resp )
+          if $self->{is_wd3};
+        return $self->commands->parse_response( $res, $resp );
+    }
+    else {
+        #Tell the use about the offending setting.
+        croak "Couldn't retrieve command settings properly ".$res->{command}."\n";
+    }
+}
+
+=head1 METHODS
+
+=head2 new_session (extra_capabilities)
+
+Make a new session on the server.
+Called by new(), not intended for regular use.
+
+Occaisonally handy for recovering from brower crashes.
+
+DANGER DANGER DANGER
+
+This will throw away your old session if you have not closed it!
+
+DANGER DANGER DANGER
+
+=cut
+
+sub new_session {
+    my ( $self, $extra_capabilities ) = @_;
+    $extra_capabilities ||= {};
+
+    my $args = {
+        'desiredCapabilities' => {
+            'browserName'       => $self->browser_name,
+            'platform'          => $self->platform,
+            'javascriptEnabled' => $self->javascript,
+            'version'           => $self->version // '',
+            'acceptSslCerts'    => $self->accept_ssl_certs,
+            %$extra_capabilities,
+        },
+    };
+    $args->{'extra_capabilities'} = \%$extra_capabilities unless $FORCE_WD2;
+
+    if ( defined $self->proxy ) {
+        $args->{desiredCapabilities}->{proxy} = $self->proxy;
+    }
+
+    if (   $args->{desiredCapabilities}->{browserName} =~ /firefox/i
+        && $self->has_firefox_profile )
+    {
+        $args->{desiredCapabilities}->{firefox_profile} =
+          $self->firefox_profile->_encode;
+    }
+
+    $self->_request_new_session($args);
+}
+
+=head2 new_desired_session(capabilities)
+
+Basically the same as new_session, but with caps.
+Sort of an analog to new_from_caps.
+
+=cut
+
+sub new_desired_session {
+    my ( $self, $caps ) = @_;
+
+    $self->_request_new_session(
+        {
+            desiredCapabilities => $caps
+        }
+    );
+}
+
+sub _request_new_session {
+    my ( $self, $args ) = @_;
+
+    #XXX UGLY shim for webdriver3
+    $args->{capabilities}->{alwaysMatch} =
+      clone( $args->{desiredCapabilities} );
+    my $cmap = $self->commands_v3->get_caps_map();
+    my $caps = $self->commands_v3->get_caps();
+    foreach my $cap ( keys( %{ $args->{capabilities}->{alwaysMatch} } ) ) {
+
+        #Handle browser specific capabilities
+        if ( exists( $args->{desiredCapabilities}->{browserName} )
+            && $cap eq 'extra_capabilities' )
+        {
+
+            if (
+                exists $args->{capabilities}->{alwaysMatch}
+                ->{'moz:firefoxOptions'}->{args} )
+            {
+                $args->{capabilities}->{alwaysMatch}->{$cap}->{args} =
+                  $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}
+                  ->{args};
+            }
+            $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'} =
+              $args->{capabilities}->{alwaysMatch}->{$cap}
+              if $args->{desiredCapabilities}->{browserName} eq 'firefox';
+
+#XXX the chrome documentation is lies, you can't do this yet
+#$args->{capabilities}->{alwaysMatch}->{'goog:chromeOptions'}      = $args->{capabilities}->{alwaysMatch}->{$cap} if $args->{desiredCapabilities}->{browserName} eq 'chrome';
+#Does not appear there are any MSIE based options, so let's just let that be
+        }
+        if (   exists( $args->{desiredCapabilities}->{browserName} )
+            && $args->{desiredCapabilities}->{browserName} eq 'firefox'
+            && $cap eq 'firefox_profile' )
+        {
+            if (
+                ref $args->{capabilities}->{alwaysMatch}->{$cap} eq
+                'Selenium::Firefox::Profile' )
+            {
+#XXX not sure if I need to keep a ref to the File::Temp::Tempdir object to prevent reaping
+                $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}
+                  ->{args} = [
+                    '-profile',
+                    $args->{capabilities}->{alwaysMatch}->{$cap}->{profile_dir}
+                      ->dirname()
+                  ];
+            }
+            else {
+           #previously undocumented feature that we can pass the encoded profile
+                $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}
+                  ->{profile} = $args->{capabilities}->{alwaysMatch}->{$cap};
+            }
+        }
+        foreach my $newkey ( keys(%$cmap) ) {
+            if ( $newkey eq $cap ) {
+                last if $cmap->{$newkey} eq $cap;
+                $args->{capabilities}->{alwaysMatch}->{ $cmap->{$newkey} } =
+                  $args->{capabilities}->{alwaysMatch}->{$cap};
+                delete $args->{capabilities}->{alwaysMatch}->{$cap};
+                last;
+            }
+        }
+        delete $args->{capabilities}->{alwaysMatch}->{$cap}
+          if !any { $_ eq $cap } @$caps;
+    }
+    delete $args->{desiredCapabilities}
+      if $FORCE_WD3;    #XXX fork working-around busted fallback in firefox
+    delete $args->{capabilities}
+      if $FORCE_WD2; #XXX 'secret' feature to help the legacy unit tests to work
+
+    #Delete compatibility layer when using drivers directly
+    if ( $self->isa('Selenium::Firefox') || $self->isa('Selenium::Chrome') || $self->isa('Selenium::Edge') ) {
+        if (   exists $args->{capabilities}
+            && exists $args->{capabilities}->{alwaysMatch} )
+        {
+            delete $args->{capabilities}->{alwaysMatch}->{browserName};
+            delete $args->{capabilities}->{alwaysMatch}->{browserVersion};
+            delete $args->{capabilities}->{alwaysMatch}->{platformName};
+        }
+    }
+
+    #Fix broken out of the box chrome because they hate the maintainers of their interfaces
+    if ( $self->isa('Selenium::Chrome') ) {
+        if ( exists $args->{desiredCapabilities} ) {
+            $args->{desiredCapabilities}{'goog:chromeOptions'}{args} //= [];
+            push(@{$args->{desiredCapabilities}{'goog:chromeOptions'}{args}}, qw{no-sandbox disable-dev-shm-usage});
+        }
+    }
+
+    # Die unless connection is good
+    my $rc = $self->remote_conn;
+    $rc->check_status();
+
+    # command => 'newSession' to fool the tests of commands implemented
+    # TODO: rewrite the testing better, this is so fragile.
+    my $resource_new_session = {
+        method => $self->commands->get_method('newSession'),
+        url    => $self->commands->get_url('newSession'),
+        no_content_success =>
+          $self->commands->get_no_content_success('newSession'),
+    };
+    my $resp = $rc->request( $resource_new_session, $args, );
+
+    if ( $resp->{cmd_status} && $resp->{cmd_status} eq 'NOT OK' ) {
+        croak "Could not obtain new session: ". $resp->{cmd_return}{message};
+    }
+
+    if ( ( defined $resp->{'sessionId'} ) && $resp->{'sessionId'} ne '' ) {
+        $self->session_id( $resp->{'sessionId'} );
+    }
+    else {
+        my $error = 'Could not create new session';
+
+        if ( ref $resp->{cmd_return} eq 'HASH' ) {
+            $error .= ': ' . $resp->{cmd_return}->{message};
+        }
+        else {
+            $error .= ': ' . $resp->{cmd_return};
+        }
+        croak $error;
+    }
+
+    #Webdriver 3 - best guess that this is 'whats goin on'
+    if ( ref $resp->{cmd_return} eq 'HASH'
+        && $resp->{cmd_return}->{capabilities} )
+    {
+        $self->{is_wd3}           = 1;
+        $self->{emulate_jsonwire} = 1;
+        $self->{capabilities}     = $resp->{cmd_return}->{capabilities};
+    }
+
+    #XXX chromedriver DOES NOT FOLLOW SPEC!
+    if ( ref $resp->{cmd_return} eq 'HASH' && $resp->{cmd_return}->{chrome} ) {
+        if ( defined $resp->{cmd_return}->{setWindowRect} )
+        {    #XXX i'm inferring we are wd3 based on the presence of this
+            $self->{is_wd3}           = 1;
+            $self->{emulate_jsonwire} = 1;
+            $self->{capabilities}     = $resp->{cmd_return};
+        }
+    }
+
+    #XXX unsurprisingly, neither does microsoft
+    if (   ref $resp->{cmd_return} eq 'HASH'
+        && $resp->{cmd_return}->{pageLoadStrategy}
+        && $self->browser_name eq 'MicrosoftEdge' )
+    {
+        $self->{is_wd3}           = 1;
+        $self->{emulate_jsonwire} = 1;
+        $self->{capabilities}     = $resp->{cmd_return};
+    }
+
+    return ( $args, $resp );
+}
+
+=head2 is_webdriver_3
+
+Print whether the server (or browser) thinks it's implemented webdriver 3.
+If this returns true, webdriver 3 methods will be used in the case an action exists in L<Selenium::Remote::Spec> for the method you are trying to call.
+If a method you are calling has no webdriver 3 equivalent (or browser extension), the legacy commands implemented in L<Selenium::Remote::Commands> will be used.
+
+Note how I said *thinks* above.  In the case you want to force usage of legacy methods, set $driver->{is_wd3} to work around various browser issues.
+
+=cut
+
+sub is_webdriver_3 {
+    my $self = shift;
+    return $self->{is_wd3};
+}
+
+=head2 debug_on
+
+  Description:
+    Turns on debugging mode and the driver will print extra info like request
+    and response to stdout. Useful, when you want to see what is being sent to
+    the server & what response you are getting back.
+
+  Usage:
+    $driver->debug_on;
+
+=cut
+
+sub debug_on {
+    my ($self) = @_;
+    $self->{debug} = 1;
+    $self->remote_conn->debug(1);
+}
+
+=head2 debug_off
+
+  Description:
+    Turns off the debugging mode.
+
+  Usage:
+    $driver->debug_off;
+
+=cut
+
+sub debug_off {
+    my ($self) = @_;
+    $self->{debug} = 0;
+    $self->remote_conn->debug(0);
+}
+
+=head2 get_sessions
+
+  Description:
+    Returns a list of the currently active sessions. Each session will be
+    returned as an array of Hashes with the following keys:
+
+    'id' : The session ID
+    'capabilities: An object describing session's capabilities
+
+  Output:
+    Array of Hashes
+
+  Usage:
+    print Dumper $driver->get_sessions();
+
+=cut
+
+sub get_sessions {
+    my ($self) = @_;
+    my $res = { 'command' => 'getSessions' };
+    return $self->_execute_command($res);
+}
+
+=head2 status
+
+  Description:
+    Query the server's current status. All server implementations
+    should return two basic objects describing the server's current
+    platform and when the server was built.
+
+  Output:
+    Hash ref
+
+  Usage:
+    print Dumper $driver->status;
+
+=cut
+
+sub status {
+    my ($self) = @_;
+    my $res = { 'command' => 'status' };
+    return $self->_execute_command($res);
+}
+
+=head2 get_alert_text
+
+ Description:
+    Gets the text of the currently displayed JavaScript alert(), confirm()
+    or prompt() dialog.
+
+ Example
+    my $string = $driver->get_alert_text;
+
+=cut
+
+sub get_alert_text {
+    my ($self) = @_;
+    my $res = { 'command' => 'getAlertText' };
+    return $self->_execute_command($res);
+}
+
+=head2 send_keys_to_active_element
+
+ Description:
+    Send a sequence of key strokes to the active element. This command is
+    similar to the send keys command in every aspect except the implicit
+    termination: The modifiers are not released at the end of the call.
+    Rather, the state of the modifier keys is kept between calls, so mouse
+    interactions can be performed while modifier keys are depressed.
+
+ Compatibility:
+    On webdriver 3 servers, don't use this to send modifier keys; use send_modifier instead.
+
+ Input: 1
+    Required:
+        {ARRAY | STRING} - Array of strings or a string.
+
+ Usage:
+    $driver->send_keys_to_active_element('abcd', 'efg');
+    $driver->send_keys_to_active_element('hijk');
+
+    or
+
+    # include the WDKeys module
+    use Selenium::Remote::WDKeys;
+    $driver->send_keys_to_active_element(KEYS->{'space'}, KEYS->{'enter'});
+
+=cut
+
+sub send_keys_to_active_element {
+    my ( $self, @strings ) = @_;
+
+    if ( $self->{is_wd3}
+        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
+    {
+        @strings = map { split( '', $_ ) } @strings;
+        my @acts = map {
+            (
+                {
+                    type  => 'keyDown',
+                    value => $_,
+                },
+                {
+                    type  => 'keyUp',
+                    value => $_,
+                }
+              )
+        } @strings;
+
+        my $action = {
+            actions => [
+                {
+                    id      => 'key',
+                    type    => 'key',
+                    actions => \@acts,
+                }
+            ]
+        };
+        return $self->general_action(%$action);
+    }
+
+    my $res    = { 'command' => 'sendKeysToActiveElement' };
+    my $params = { 'value'   => \@strings, };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 send_keys_to_alert
+
+Synonymous with send_keys_to_prompt
+
+=cut
+
+sub send_keys_to_alert {
+    return shift->send_keys_to_prompt(@_);
+}
+
+=head2 send_keys_to_prompt
+
+ Description:
+    Sends keystrokes to a JavaScript prompt() dialog.
+
+ Input:
+    {string} keys to send
+
+ Example:
+    $driver->send_keys_to_prompt('hello world');
+  or
+    ok($driver->get_alert_text eq 'Please Input your name','prompt appears');
+    $driver->send_keys_to_alert("Larry Wall");
+    $driver->accept_alert;
+
+=cut
+
+sub send_keys_to_prompt {
+    my ( $self, $keys ) = @_;
+    my $res    = { 'command' => 'sendKeysToPrompt' };
+    my $params = { 'text'    => $keys };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 accept_alert
+
+ Description:
+    Accepts the currently displayed alert dialog.  Usually, this is
+    equivalent to clicking the 'OK' button in the dialog.
+
+ Example:
+    $driver->accept_alert;
+
+=cut
+
+sub accept_alert {
+    my ($self) = @_;
+    my $res = { 'command' => 'acceptAlert' };
+    return $self->_execute_command($res);
+}
+
+=head2 dismiss_alert
+
+ Description:
+    Dismisses the currently displayed alert dialog. For comfirm()
+    and prompt() dialogs, this is equivalent to clicking the
+    'Cancel' button. For alert() dialogs, this is equivalent to
+    clicking the 'OK' button.
+
+ Example:
+    $driver->dismiss_alert;
+
+=cut
+
+sub dismiss_alert {
+    my ($self) = @_;
+    my $res = { 'command' => 'dismissAlert' };
+    return $self->_execute_command($res);
+}
+
+=head2 general_action
+
+Provide an 'actions definition' hash to make webdriver use input devices.
+Given the spec for the structure of this data is 'non normative',
+it is left as an exercise to the reader what that means as to how to use this function.
+
+That said, it seems most of the data looks something like this:
+
+    $driver->general_action( actions => [{
+        type => 'pointer|key|none|somethingElseSuperSpecialDefinedByYourBrowserDriver',
+        id => MUST be mouse|key|none|other.  And by 'other' I mean anything else.  The first 3 are 'special' in that they are used in the global actions queue.
+              If you want say, another mouse action to execute in parallel to other mouse actions (to simulate multi-touch, for example), call your action 'otherMouseAction' or something.
+        parameters => {
+            someOption => "basically these are global parameters used by all steps in the forthcoming "action chain".
+        },
+        actions => [
+            {
+                type => "keyUp|KeyDown if key, pointerUp|pointerDown|pointerMove|pointerCancel if pointer, pause if any type",
+                key => A raw keycode or character from the keyboard if this is a key event,
+                duration => how many 'ticks' this action should take, you probably want this to be 0 all of the time unless you are evading Software debounce.
+                button => what number button if you are using a pointer (this sounds terribly like it might be re-purposed to be a joypad in the future sometime)
+                origin => Point of Origin if moving a pointer around
+                x => unit vector to travel along x-axis if pointerMove event
+                y => unit vector to travel along y-axis if pointerMove event
+            },
+            ...
+        ]
+        },
+        ...
+        ]
+    )
+
+Only available on WebDriver3 capable selenium servers.
+
+If you have called any legacy shim, such as mouse_move_to_location() previously, your actions passed will be appended to the existing actions queue.
+Called with no arguments, it simply executes the existing action queue.
+
+If you are looking for pre-baked action chains that aren't currently part of L<Selenium::Remote::Driver>,
+consider L<Selenium::ActionChains>, which is shipped with this distribution instead.
+
+=head3 COMPATIBILITY
+
+Like most places, the WC3 standard is openly ignored by the driver binaries.
+Generally an "actions" object will only accept:
+
+    { type => ..., value => ... }
+
+When using the direct drivers (E.G. Selenium::Chrome, Selenium::Firefox).
+This is not documented anywhere but here, as far as I can tell.
+
+=cut
+
+sub general_action {
+    my ( $self, %action ) = @_;
+
+    _queue_action(%action);
+    my $res = { 'command' => 'generalAction' };
+    my $out = $self->_execute_command( $res, \%CURRENT_ACTION_CHAIN );
+    %CURRENT_ACTION_CHAIN = ( actions => [] );
+    return $out;
+}
+
+sub _queue_action {
+    my (%action) = @_;
+    if ( ref $action{actions} eq 'ARRAY' ) {
+        foreach my $live_action ( @{ $action{actions} } ) {
+            my $existing_action;
+            foreach my $global_action ( @{ $CURRENT_ACTION_CHAIN{actions} } ) {
+                if ( $global_action->{id} eq $live_action->{id} ) {
+                    $existing_action = $global_action;
+                    last;
+                }
+            }
+            if ($existing_action) {
+                push(
+                    @{ $existing_action->{actions} },
+                    @{ $live_action->{actions} }
+                );
+            }
+            else {
+                push( @{ $CURRENT_ACTION_CHAIN{actions} }, $live_action );
+            }
+        }
+    }
+}
+
+=head2 release_general_action
+
+Nukes *all* input device state (modifier key up/down, pointer button up/down, pointer location, and other device state) from orbit.
+Call if you forget to do a *Up event in your provided action chains, or just to save time.
+
+Also clears the current actions queue.
+
+Only available on WebDriver3 capable selenium servers.
+
+=cut
+
+sub release_general_action {
+    my ($self) = @_;
+    my $res = { 'command' => 'releaseGeneralAction' };
+    %CURRENT_ACTION_CHAIN = ( actions => [] );
+    return $self->_execute_command($res);
+}
+
+=head2 mouse_move_to_location
+
+ Description:
+    Move the mouse by an offset of the specificed element. If no
+    element is specified, the move is relative to the current mouse
+    cursor. If an element is provided but no offset, the mouse will be
+    moved to the center of the element. If the element is not visible,
+    it will be scrolled into view.
+
+ Compatibility:
+    Due to limitations in the Webdriver 3 API, mouse movements have to be executed 'lazily' e.g. only right before a click() event occurs.
+    This is because there is no longer any persistent mouse location state; mouse movements are now totally atomic.
+    This has several problematic aspects; for one, I can't think of a way to both hover an element and then do another action relying on the element staying hover()ed,
+    Aside from using javascript workarounds.
+
+ Output:
+    STRING -
+
+ Usage:
+    # element - the element to move to. If not specified or is null, the offset is relative to current position of the mouse.
+    # xoffset - X offset to move to, relative to the top-left corner of the element. If not specified, the mouse will move to the middle of the element.
+    # yoffset - Y offset to move to, relative to the top-left corner of the element. If not specified, the mouse will move to the middle of the element.
+
+    print $driver->mouse_move_to_location(element => e, xoffset => x, yoffset => y);
+
+=cut
+
+sub mouse_move_to_location {
+    my ( $self, %params ) = @_;
+    $params{element} = $params{element}{id} if exists $params{element};
+
+    if ( $self->{is_wd3}
+        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
+    {
+        my $origin      = $params{element};
+        my $move_action = {
+            type     => "pointerMove",
+            duration => 0,
+            x        => $params{xoffset} // 0,
+            y        => $params{yoffset} // 0,
+        };
+        $move_action->{origin} =
+          { 'element-6066-11e4-a52e-4f735466cecf' => $origin }
+          if $origin;
+
+        _queue_action(
+            actions => [
+                {
+                    type         => "pointer",
+                    id           => 'mouse',
+                    "parameters" => { "pointerType" => "mouse" },
+                    actions      => [$move_action],
+                }
+            ]
+        );
+        return 1;
+    }
+
+    my $res = { 'command' => 'mouseMoveToLocation' };
+    return $self->_execute_command( $res, \%params );
+}
+
+=head2 move_to
+
+Synonymous with mouse_move_to_location
+
+=cut
+
+sub move_to {
+    return shift->mouse_move_to_location(@_);
+}
+
+=head2 get_capabilities
+
+ Description:
+    Retrieve the capabilities of the specified session.
+
+ Output:
+    HASH of all the capabilities.
+
+ Usage:
+    my $capab = $driver->get_capabilities();
+    print Dumper($capab);
+
+=cut
+
+sub get_capabilities {
+    my $self = shift;
+    my $res = { 'command' => 'getCapabilities' };
+    return $self->_execute_command($res);
+}
+
+=head2 get_timeouts
+
+  Description:
+    Get the currently configured values (ms) for the page load, script and implicit timeouts.
+
+  Compatibility:
+    Only available on WebDriver3 enabled selenium servers.
+
+  Usage:
+    $driver->get_timeouts();
+
+=cut
+
+sub get_timeouts {
+    my $self = shift;
+    my $res = { 'command' => 'getTimeouts' };
+    return $self->_execute_command( $res, {} );
+}
+
+=head2 set_timeout
+
+ Description:
+    Configure the amount of time that a particular type of operation can execute
+    for before they are aborted and a |Timeout| error is returned to the client.
+
+ Input:
+    type - <STRING> - The type of operation to set the timeout for.
+                      Valid values are:
+                      "script"    : for script timeouts,
+                      "implicit"  : for modifying the implicit wait timeout
+                      "page load" : for setting a page load timeout.
+    ms - <NUMBER> - The amount of time, in milliseconds, that time-limited
+            commands are permitted to run.
+
+ Usage:
+    $driver->set_timeout('script', 1000);
+
+=cut
+
+sub set_timeout {
+    my ( $self, $type, $ms ) = @_;
+    if ( not defined $type ) {
+        croak "Expecting type";
+    }
+    $ms   = _coerce_timeout_ms($ms);
+    $type = 'pageLoad'
+      if $type eq 'page load'
+      && $self->browser_name ne
+      'MicrosoftEdge';    #XXX SHIM they changed the WC3 standard mid stream
+
+    my $res    = { 'command' => 'setTimeout' };
+    my $params = { $type     => $ms };
+
+    #XXX edge still follows earlier versions of the WC3 standard
+    if ( $self->browser_name eq 'MicrosoftEdge' ) {
+        $params->{ms}   = $ms;
+        $params->{type} = $type;
+    }
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 set_async_script_timeout
+
+ Description:
+    Set the amount of time, in milliseconds, that asynchronous scripts executed
+    by execute_async_script() are permitted to run before they are
+    aborted and a |Timeout| error is returned to the client.
+
+ Input:
+    ms - <NUMBER> - The amount of time, in milliseconds, that time-limited
+            commands are permitted to run.
+
+ Usage:
+    $driver->set_async_script_timeout(1000);
+
+=cut
+
+sub set_async_script_timeout {
+    my ( $self, $ms ) = @_;
+
+    return $self->set_timeout( 'script', $ms ) if $self->{is_wd3};
+
+    $ms = _coerce_timeout_ms($ms);
+    my $res    = { 'command' => 'setAsyncScriptTimeout' };
+    my $params = { 'ms'      => $ms };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 set_implicit_wait_timeout
+
+ Description:
+    Set the amount of time the driver should wait when searching for elements.
+    When searching for a single element, the driver will poll the page until
+    an element is found or the timeout expires, whichever occurs first.
+    When searching for multiple elements, the driver should poll the page until
+    at least one element is found or the timeout expires, at which point it
+    will return an empty list. If this method is never called, the driver will
+    default to an implicit wait of 0ms.
+
+    This is exactly equivalent to calling L</set_timeout> with a type
+    arg of C<"implicit">.
+
+ Input:
+    Time in milliseconds.
+
+ Output:
+    Server Response Hash with no data returned back from the server.
+
+ Usage:
+    $driver->set_implicit_wait_timeout(10);
+
+=cut
+
+sub set_implicit_wait_timeout {
+    my ( $self, $ms ) = @_;
+    return $self->set_timeout( 'implicit', $ms ) if $self->{is_wd3};
+
+    $ms = _coerce_timeout_ms($ms);
+    my $res    = { 'command' => 'setImplicitWaitTimeout' };
+    my $params = { 'ms'      => $ms };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 pause
+
+ Description:
+    Pause execution for a specified interval of milliseconds.
+
+ Usage:
+    $driver->pause(10000);  # 10 second delay
+    $driver->pause();       #  1 second delay default
+
+ DEPRECATED: consider using Time::HiRes instead.
+
+=cut
+
+sub pause {
+    my $self = shift;
+    my $timeout = ( shift // 1000 ) * 1000;
+    usleep($timeout);
+}
+
+=head2 close
+
+ Description:
+    Close the current window.
+
+ Usage:
+    $driver->close();
+ or
+    #close a popup window
+    my $handles = $driver->get_window_handles;
+    $driver->switch_to_window($handles->[1]);
+    $driver->close();
+    $driver->switch_to_window($handles->[0]);
+
+=cut
+
+sub close {
+    my $self = shift;
+    my $res = { 'command' => 'close' };
+    $self->_execute_command($res);
+}
+
+=head2 quit
+
+ Description:
+    DELETE the session, closing open browsers. We will try to call
+    this on our down when we get destroyed, but in the event that we
+    are demolished during global destruction, we will not be able to
+    close the browser. For your own unattended and/or complicated
+    tests, we recommend explicitly calling quit to make sure you're
+    not leaving orphan browsers around.
+
+    Note that as a Moo class, we use a subroutine called DEMOLISH that
+    takes the place of DESTROY; for more information, see
+    https://metacpan.org/pod/Moo#DEMOLISH.
+
+ Usage:
+    $driver->quit();
+
+=cut
+
+sub quit {
+    my $self = shift;
+    my $res = { 'command' => 'quit' };
+    $self->_execute_command($res);
+    $self->session_id(undef);
+}
+
+=head2 get_current_window_handle
+
+ Description:
+    Retrieve the current window handle.
+
+ Output:
+    STRING - the window handle
+
+ Usage:
+    print $driver->get_current_window_handle();
+
+=cut
+
+sub get_current_window_handle {
+    my $self = shift;
+    my $res = { 'command' => 'getCurrentWindowHandle' };
+    return $self->_execute_command($res);
+}
+
+=head2 get_window_handles
+
+ Description:
+    Retrieve the list of window handles used in the session.
+
+ Output:
+    ARRAY of STRING - list of the window handles
+
+ Usage:
+    print Dumper $driver->get_window_handles;
+ or
+    # get popup, close, then back
+    my $handles = $driver->get_window_handles;
+    $driver->switch_to_window($handles->[1]);
+    $driver->close;
+    $driver->switch_to_window($handles->[0]);
+
+=cut
+
+sub get_window_handles {
+    my $self = shift;
+    my $res = { 'command' => 'getWindowHandles' };
+    return $self->_execute_command($res);
+}
+
+=head2 get_window_size
+
+ Description:
+    Retrieve the window size
+
+ Compatibility:
+    The ability to get the size of arbitrary handles by passing input only exists in WebDriver2.
+    You will have to switch to the window first going forward.
+
+ Input:
+    STRING - <optional> - window handle (default is 'current' window)
+
+ Output:
+    HASH - containing keys 'height' & 'width'
+
+ Usage:
+    my $window_size = $driver->get_window_size();
+    print $window_size->{'height'}, $window_size->{'width'};
+
+=cut
+
+sub get_window_size {
+    my ( $self, $window ) = @_;
+    $window = ( defined $window ) ? $window : 'current';
+    my $res = { 'command' => 'getWindowSize', 'window_handle' => $window };
+    $res = { 'command' => 'getWindowRect', handle => $window }
+      if $self->{is_wd3};
+    return $self->_execute_command($res);
+}
+
+=head2 get_window_position
+
+ Description:
+    Retrieve the window position
+
+ Compatibility:
+    The ability to get the size of arbitrary handles by passing input only exists in WebDriver2.
+    You will have to switch to the window first going forward.
+
+ Input:
+    STRING - <optional> - window handle (default is 'current' window)
+
+ Output:
+    HASH - containing keys 'x' & 'y'
+
+ Usage:
+    my $window_size = $driver->get_window_position();
+    print $window_size->{'x'}, $window_size->('y');
+
+=cut
+
+sub get_window_position {
+    my ( $self, $window ) = @_;
+    $window = ( defined $window ) ? $window : 'current';
+    my $res = { 'command' => 'getWindowPosition', 'window_handle' => $window };
+    $res = { 'command' => 'getWindowRect', handle => $window }
+      if $self->{is_wd3};
+    return $self->_execute_command($res);
+}
+
+=head2 get_current_url
+
+ Description:
+    Retrieve the url of the current page
+
+ Output:
+    STRING - url
+
+ Usage:
+    print $driver->get_current_url();
+
+=cut
+
+sub get_current_url {
+    my $self = shift;
+    my $res = { 'command' => 'getCurrentUrl' };
+    return $self->_execute_command($res);
+}
+
+=head2 navigate
+
+ Description:
+    Navigate to a given url. This is same as get() method.
+
+ Input:
+    STRING - url
+
+ Usage:
+    $driver->navigate('http://www.google.com');
+
+=cut
+
+sub navigate {
+    my ( $self, $url ) = @_;
+    $self->get($url);
+}
+
+=head2 get
+
+ Description:
+    Navigate to a given url
+
+ Input:
+    STRING - url
+
+ Usage:
+    $driver->get('http://www.google.com');
+
+=cut
+
+sub get {
+    my ( $self, $url ) = @_;
+
+    if ( $self->has_base_url && $url !~ m|://| ) {
+        $url =~ s|^/||;
+        $url = $self->base_url . "/" . $url;
+    }
+
+    my $res    = { 'command' => 'get' };
+    my $params = { 'url'     => $url };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 get_title
+
+ Description:
+    Get the current page title
+
+ Output:
+    STRING - Page title
+
+ Usage:
+    print $driver->get_title();
+
+=cut
+
+sub get_title {
+    my $self = shift;
+    my $res = { 'command' => 'getTitle' };
+    return $self->_execute_command($res);
+}
+
+=head2 go_back
+
+ Description:
+    Equivalent to hitting the back button on the browser.
+
+ Usage:
+    $driver->go_back();
+
+=cut
+
+sub go_back {
+    my $self = shift;
+    my $res = { 'command' => 'goBack' };
+    return $self->_execute_command($res);
+}
+
+=head2 go_forward
+
+ Description:
+    Equivalent to hitting the forward button on the browser.
+
+ Usage:
+    $driver->go_forward();
+
+=cut
+
+sub go_forward {
+    my $self = shift;
+    my $res = { 'command' => 'goForward' };
+    return $self->_execute_command($res);
+}
+
+=head2 refresh
+
+ Description:
+    Reload the current page.
+
+ Usage:
+    $driver->refresh();
+
+=cut
+
+sub refresh {
+    my $self = shift;
+    my $res = { 'command' => 'refresh' };
+    return $self->_execute_command($res);
+}
+
+=head2 has_javascript
+
+ Description:
+    returns true if javascript is enabled in the driver.
+
+ Compatibility:
+    Can't be false on WebDriver 3.
+
+ Usage:
+    if ($driver->has_javascript) { ...; }
+
+=cut
+
+sub has_javascript {
+    my $self = shift;
+    return int( $self->javascript );
+}
+
+=head2 execute_async_script
+
+ Description:
+    Inject a snippet of JavaScript into the page for execution in the context
+    of the currently selected frame. The executed script is assumed to be
+    asynchronous and must signal that is done by invoking the provided
+    callback, which is always provided as the final argument to the function.
+    The value to this callback will be returned to the client.
+
+    Asynchronous script commands may not span page loads. If an unload event
+    is fired while waiting for a script result, an error should be returned
+    to the client.
+
+ Input: 2 (1 optional)
+    Required:
+        STRING - Javascript to execute on the page
+    Optional:
+        ARRAY - list of arguments that need to be passed to the script.
+
+ Output:
+    {*} - Varied, depending on the type of result expected back from the script.
+
+ Usage:
+    my $script = q{
+        var arg1 = arguments[0];
+        var callback = arguments[arguments.length-1];
+        var elem = window.document.findElementById(arg1);
+        callback(elem);
+    };
+    my $elem = $driver->execute_async_script($script,'myid');
+    $elem->click;
+
+=cut
+
+sub execute_async_script {
+    my ( $self, $script, @args ) = @_;
+    if ( $self->has_javascript ) {
+        if ( not defined $script ) {
+            croak 'No script provided';
+        }
+        my $res =
+          { 'command' => 'executeAsyncScript' . $self->_execute_script_suffix };
+
+        # Check the args array if the elem obj is provided & replace it with
+        # JSON representation
+        for ( my $i = 0 ; $i < @args ; $i++ ) {
+            if ( Scalar::Util::blessed( $args[$i] )
+                and $args[$i]->isa('Selenium::Remote::WebElement') )
+            {
+                if ( $self->{is_wd3} ) {
+                    $args[$i] =
+                      { 'element-6066-11e4-a52e-4f735466cecf' =>
+                          ( $args[$i] )->{id} };
+                }
+                else {
+                    $args[$i] = { 'ELEMENT' => ( $args[$i] )->{id} };
+                }
+            }
+        }
+
+        my $params = { 'script' => $script, 'args' => \@args };
+        my $ret = $self->_execute_command( $res, $params );
+
+        # replace any ELEMENTS with WebElement
+        if (    ref($ret)
+            and ( ref($ret) eq 'HASH' )
+            and $self->_looks_like_element($ret) )
+        {
+            $ret = $self->webelement_class->new(
+                id     => $ret,
+                driver => $self
+            );
+        }
+        return $ret;
+    }
+    else {
+        croak 'Javascript is not enabled on remote driver instance.';
+    }
+}
+
+=head2 execute_script
+
+ Description:
+    Inject a snippet of JavaScript into the page and return its result.
+    WebElements that should be passed to the script as an argument should be
+    specified in the arguments array as WebElement object. Likewise,
+    any WebElements in the script result will be returned as WebElement object.
+
+ Input: 2 (1 optional)
+    Required:
+        STRING - Javascript to execute on the page
+    Optional:
+        ARRAY - list of arguments that need to be passed to the script.
+
+ Output:
+    {*} - Varied, depending on the type of result expected back from the script.
+
+ Usage:
+    my $script = q{
+        var arg1 = arguments[0];
+        var elem = window.document.findElementById(arg1);
+        return elem;
+    };
+    my $elem = $driver->execute_script($script,'myid');
+    $elem->click;
+
+=cut
+
+sub execute_script {
+    my ( $self, $script, @args ) = @_;
+    if ( $self->has_javascript ) {
+        if ( not defined $script ) {
+            croak 'No script provided';
+        }
+        my $res =
+          { 'command' => 'executeScript' . $self->_execute_script_suffix };
+
+        # Check the args array if the elem obj is provided & replace it with
+        # JSON representation
+        for ( my $i = 0 ; $i < @args ; $i++ ) {
+            if ( Scalar::Util::blessed( $args[$i] )
+                and $args[$i]->isa('Selenium::Remote::WebElement') )
+            {
+                if ( $self->{is_wd3} ) {
+                    $args[$i] =
+                      { 'element-6066-11e4-a52e-4f735466cecf' =>
+                          ( $args[$i] )->{id} };
+                }
+                else {
+                    $args[$i] = { 'ELEMENT' => ( $args[$i] )->{id} };
+                }
+            }
+        }
+
+        my $params = { 'script' => $script, 'args' => [@args] };
+        my $ret = $self->_execute_command( $res, $params );
+
+        return $self->_convert_to_webelement($ret);
+    }
+    else {
+        croak 'Javascript is not enabled on remote driver instance.';
+    }
+}
+
+# _looks_like_element
+# An internal method to check if a return value might be an element
+
+sub _looks_like_element {
+    my ( $self, $maybe_element ) = @_;
+
+    return (
+             exists $maybe_element->{ELEMENT}
+          or exists $maybe_element->{'element-6066-11e4-a52e-4f735466cecf'}
+    );
+}
+
+# _convert_to_webelement
+# An internal method used to traverse a data structure
+# and convert any ELEMENTS with WebElements
+
+sub _convert_to_webelement {
+    my ( $self, $ret ) = @_;
+
+    if ( ref($ret) and ( ref($ret) eq 'HASH' ) ) {
+        if ( $self->_looks_like_element($ret) ) {
+
+            # replace an ELEMENT with WebElement
+            return $self->webelement_class->new(
+                id     => $ret,
+                driver => $self
+            );
+        }
+
+        my %hash;
+        foreach my $key ( keys %$ret ) {
+            $hash{$key} = $self->_convert_to_webelement( $ret->{$key} );
+        }
+        return \%hash;
+    }
+
+    if ( ref($ret) and ( ref($ret) eq 'ARRAY' ) ) {
+        my @array = map { $self->_convert_to_webelement($_) } @$ret;
+        return \@array;
+    }
+
+    return $ret;
+}
+
+=head2 screenshot
+
+ Description:
+    Get a screenshot of the current page as a base64 encoded image.
+    Optionally pass {'full' => 1} as argument to take a full screenshot and not
+    only the viewport. (Works only with firefox and geckodriver >= 0.24.0)
+
+ Output:
+    STRING - base64 encoded image
+
+ Usage:
+    print $driver->screenshot();
+    print $driver->screenshot({'full' => 1});
+
+To conveniently write the screenshot to a file, see L</capture_screenshot>.
+
+=cut
+
+sub screenshot {
+    my ($self, $params) = @_;
+    $params //= { full => 0 };
+
+    croak "Full page screenshot only supported on geckodriver" if $params->{full} && ( $self->{browser_name} ne 'firefox' );
+
+    my $res = { 'command' => $params->{'full'} == 1 ? 'mozScreenshotFull' : 'screenshot' };
+    return $self->_execute_command($res);
+}
+
+=head2 capture_screenshot
+
+ Description:
+    Capture a screenshot and save as a PNG to provided file name.
+    (The method is compatible with the WWW::Selenium method of the same name)
+    Optionally pass {'full' => 1} as second argument to take a full screenshot
+    and not only the viewport. (Works only with firefox and geckodriver >= 0.24.0)
+
+ Output:
+    TRUE - (Screenshot is written to file)
+
+ Usage:
+    $driver->capture_screenshot($filename);
+    $driver->capture_screenshot($filename, {'full' => 1});
+
+=cut
+
+sub capture_screenshot {
+    my ( $self, $filename, $params ) = @_;
+    croak '$filename is required' unless $filename;
+
+    open( my $fh, '>', $filename );
+    binmode $fh;
+    print $fh MIME::Base64::decode_base64( $self->screenshot($params) );
+    CORE::close $fh;
+    return 1;
+}
+
+=head2 available_engines
+
+ Description:
+    List all available engines on the machine. To use an engine, it has to be present in this list.
+
+ Compatibility:
+    Does not appear to be available on Webdriver3 enabled selenium servers.
+
+ Output:
+    {Array.<string>} A list of available engines
+
+ Usage:
+    print Dumper $driver->available_engines;
+
+=cut
+
+#TODO emulate behavior on wd3?
+#grep { eval { Selenium::Remote::Driver->new( browser => $_ ) } } (qw{firefox MicrosoftEdge chrome opera safari htmlunit iphone phantomjs},'internet_explorer');
+#might do the trick
+sub available_engines {
+    my ($self) = @_;
+    my $res = { 'command' => 'availableEngines' };
+    return $self->_execute_command($res);
+}
+
+=head2 switch_to_frame
+
+ Description:
+    Change focus to another frame on the page. If the frame ID is null, the
+    server will switch to the page's default content. You can also switch to a
+    WebElement, for e.g. you can find an iframe using find_element & then
+    provide that as an input to this method. Also see e.g.
+
+ Input: 1
+    Required:
+        {STRING | NUMBER | NULL | WebElement} - ID of the frame which can be one of the three
+                                   mentioned.
+
+ Usage:
+    $driver->switch_to_frame('frame_1');
+    or
+    $driver->switch_to_frame($driver->find_element('iframe', 'tag_name'));
+
+=head3 COMPATIBILITY
+
+Chromedriver will vomit if you pass anything but a webElement, so you probably should do that from now on.
+
+=cut
+
+sub switch_to_frame {
+    my ( $self, $id ) = @_;
+
+    my $json_null = JSON::null;
+    my $params;
+    $id = ( defined $id ) ? $id : $json_null;
+
+    my $res = { 'command' => 'switchToFrame' };
+
+    if ( ref $id eq $self->webelement_class ) {
+        if ( $self->{is_wd3} ) {
+            $params =
+              { 'id' =>
+                  { 'element-6066-11e4-a52e-4f735466cecf' => $id->{'id'} } };
+        }
+        else {
+            $params = { 'id' => { 'ELEMENT' => $id->{'id'} } };
+        }
+    }
+    else {
+        $params = { 'id' => $id };
+    }
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 switch_to_parent_frame
+
+Webdriver 3 equivalent of calling switch_to_frame with no arguments (e.g. NULL frame).
+This is actually called in that case, supposing you are using WD3 capable servers now.
+
+=cut
+
+sub switch_to_parent_frame {
+    my ($self) = @_;
+    my $res = { 'command' => 'switchToParentFrame' };
+    return $self->_execute_command($res);
+}
+
+=head2 switch_to_window
+
+ Description:
+    Change focus to another window. The window to change focus to may
+    be specified by its server assigned window handle, or by the value
+    of the page's window.name attribute.
+
+    If you wish to use the window name as the target, you'll need to
+    have set C<window.name> on the page either in app code or via
+    L</execute_script>, or pass a name as the second argument to the
+    C<window.open()> function when opening the new window. Note that
+    the window name used here has nothing to do with the window title,
+    or the C<< <title> >> element on the page.
+
+    Otherwise, use L</get_window_handles> and select a
+    Webdriver-generated handle from the output of that function.
+
+ Input: 1
+    Required:
+        STRING - Window handle or the Window name
+
+ Usage:
+    $driver->switch_to_window('MY Homepage');
+ or
+    # close a popup window and switch back
+    my $handles = $driver->get_window_handles;
+    $driver->switch_to_window($handles->[1]);
+    $driver->close;
+    $driver->switch_to_window($handles->[0]);
+
+=cut
+
+sub switch_to_window {
+    my ( $self, $name ) = @_;
+    if ( not defined $name ) {
+        return 'Window name not provided';
+    }
+    my $res = { 'command' => 'switchToWindow' };
+    my $params = { 'name' => $name, 'handle' => $name };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 set_window_position
+
+ Description:
+    Set the position (on screen) where you want your browser to be displayed.
+
+ Compatibility:
+    In webDriver 3 enabled selenium servers, you may only operate on the focused window.
+    As such, the window handle argument below will be ignored in this context.
+
+ Input:
+    INT - x co-ordinate
+    INT - y co-ordinate
+    STRING - <optional> - window handle (default is 'current' window)
+
+ Output:
+    BOOLEAN - Success or failure
+
+ Usage:
+    $driver->set_window_position(50, 50);
+
+=cut
+
+sub set_window_position {
+    my ( $self, $x, $y, $window ) = @_;
+    $window = ( defined $window ) ? $window : 'current';
+    if ( not defined $x and not defined $y ) {
+        croak "X & Y co-ordinates are required";
+    }
+    croak qq{Error: In set_window_size, argument x "$x" isn't numeric}
+      unless Scalar::Util::looks_like_number($x);
+    croak qq{Error: In set_window_size, argument y "$y" isn't numeric}
+      unless Scalar::Util::looks_like_number($y);
+    $x +=
+      0;  # convert to numeric if a string, otherwise they'll be sent as strings
+    $y += 0;
+    my $res = { 'command' => 'setWindowPosition', 'window_handle' => $window };
+    my $params = { 'x' => $x, 'y' => $y };
+    if ( $self->{is_wd3} ) {
+        $res = { 'command' => 'setWindowRect', handle => $window };
+    }
+    my $ret = $self->_execute_command( $res, $params );
+    return $ret ? 1 : 0;
+}
+
+=head2 set_window_size
+
+ Description:
+    Set the size of the browser window
+
+ Compatibility:
+    In webDriver 3 enabled selenium servers, you may only operate on the focused window.
+    As such, the window handle argument below will be ignored in this context.
+
+ Input:
+    INT - height of the window
+    INT - width of the window
+    STRING - <optional> - window handle (default is 'current' window)
+
+ Output:
+    BOOLEAN - Success or failure
+
+ Usage:
+    $driver->set_window_size(640, 480);
+
+=cut
+
+sub set_window_size {
+    my ( $self, $height, $width, $window ) = @_;
+    $window = ( defined $window ) ? $window : 'current';
+    if ( not defined $height and not defined $width ) {
+        croak "height & width of browser are required";
+    }
+    croak qq{Error: In set_window_size, argument height "$height" isn't numeric}
+      unless Scalar::Util::looks_like_number($height);
+    croak qq{Error: In set_window_size, argument width "$width" isn't numeric}
+      unless Scalar::Util::looks_like_number($width);
+    $height +=
+      0;  # convert to numeric if a string, otherwise they'll be sent as strings
+    $width += 0;
+    my $res = { 'command' => 'setWindowSize', 'window_handle' => $window };
+    my $params = { 'height' => $height, 'width' => $width };
+    if ( $self->{is_wd3} ) {
+        $res = { 'command' => 'setWindowRect', handle => $window };
+    }
+    my $ret = $self->_execute_command( $res, $params );
+    return $ret ? 1 : 0;
+}
+
+=head2 maximize_window
+
+ Description:
+    Maximizes the browser window
+
+ Compatibility:
+    In webDriver 3 enabled selenium servers, you may only operate on the focused window.
+    As such, the window handle argument below will be ignored in this context.
+
+    Also, on chromedriver maximize is actually just setting the window size to the screen's
+    available height and width.
+
+ Input:
+    STRING - <optional> - window handle (default is 'current' window)
+
+ Output:
+    BOOLEAN - Success or failure
+
+ Usage:
+    $driver->maximize_window();
+
+=cut
+
+sub maximize_window {
+    my ( $self, $window ) = @_;
+
+    $window = ( defined $window ) ? $window : 'current';
+    my $res = { 'command' => 'maximizeWindow', 'window_handle' => $window };
+    my $ret = $self->_execute_command($res);
+    return $ret ? 1 : 0;
+}
+
+=head2 minimize_window
+
+ Description:
+    Minimizes the currently focused browser window (webdriver3 only)
+
+ Output:
+    BOOLEAN - Success or failure
+
+ Usage:
+    $driver->minimize_window();
+
+=cut
+
+sub minimize_window {
+    my ( $self, $window ) = @_;
+    $window = ( defined $window ) ? $window : 'current';
+    my $res = { 'command' => 'minimizeWindow', 'window_handle' => $window };
+    my $ret = $self->_execute_command($res);
+    return $ret ? 1 : 0;
+}
+
+=head2 fullscreen_window
+
+ Description:
+    Fullscreens the currently focused browser window (webdriver3 only)
+
+ Output:
+    BOOLEAN - Success or failure
+
+ Usage:
+    $driver->fullscreen_window();
+
+=cut
+
+sub fullscreen_window {
+    my ( $self, $window ) = @_;
+    $window = ( defined $window ) ? $window : 'current';
+    my $res = { 'command' => 'fullscreenWindow', 'window_handle' => $window };
+    my $ret = $self->_execute_command($res);
+    return $ret ? 1 : 0;
+}
+
+=head2 get_all_cookies
+
+ Description:
+    Retrieve all cookies visible to the current page. Each cookie will be
+    returned as a HASH reference with the following keys & their value types:
+
+    'name' - STRING
+    'value' - STRING
+    'path' - STRING
+    'domain' - STRING
+    'secure' - BOOLEAN
+
+ Output:
+    ARRAY of HASHES - list of all the cookie hashes
+
+ Usage:
+    print Dumper($driver->get_all_cookies());
+
+=cut
+
+sub get_all_cookies {
+    my ($self) = @_;
+    my $res = { 'command' => 'getAllCookies' };
+    return $self->_execute_command($res);
+}
+
+=head2 add_cookie
+
+ Description:
+    Set a cookie on the domain.
+
+ Input: 2 (4 optional)
+    Required:
+        'name'   - STRING
+        'value'  - STRING
+
+    Optional:
+        'path'   - STRING
+        'domain' - STRING
+        'secure'   - BOOLEAN - default false.
+        'httponly' - BOOLEAN - default false.
+        'expiry'   - TIME_T  - default 20 years in the future
+
+ Usage:
+    $driver->add_cookie('foo', 'bar', '/', '.google.com', 0, 1)
+
+=cut
+
+sub add_cookie {
+    my ( $self, $name, $value, $path, $domain, $secure, $httponly, $expiry ) =
+      @_;
+
+    if (   ( not defined $name )
+        || ( not defined $value ) )
+    {
+        croak "Missing parameters";
+    }
+
+    my $res        = { 'command' => 'addCookie' };
+    my $json_false = JSON::false;
+    my $json_true  = JSON::true;
+    $secure = ( defined $secure && $secure ) ? $json_true : $json_false;
+
+    my $params = {
+        'cookie' => {
+            'name'   => $name,
+            'value'  => $value,
+            'path'   => $path,
+            'secure' => $secure,
+        }
+    };
+    $params->{cookie}->{domain}     = $domain   if $domain;
+    $params->{cookie}->{'httponly'} = $httponly if $httponly;
+    $params->{cookie}->{'expiry'}   = $expiry   if $expiry;
+
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 delete_all_cookies
+
+ Description:
+    Delete all cookies visible to the current page.
+
+ Usage:
+    $driver->delete_all_cookies();
+
+=cut
+
+sub delete_all_cookies {
+    my ($self) = @_;
+    my $res = { 'command' => 'deleteAllCookies' };
+    return $self->_execute_command($res);
+}
+
+=head2 get_cookie_named
+
+Basically get only the cookie with the provided name.
+Probably preferable to pick it out of the list unless you expect a *really* long list.
+
+ Input:
+    Cookie Name - STRING
+
+Returns cookie definition hash, much like the elements in get_all_cookies();
+
+  Compatibility:
+    Only available on webdriver3 enabled selenium servers.
+
+=cut
+
+sub get_cookie_named {
+    my ( $self, $cookie_name ) = @_;
+    my $res = { 'command' => 'getCookieNamed', 'name' => $cookie_name };
+    return $self->_execute_command($res);
+}
+
+=head2 delete_cookie_named
+
+ Description:
+    Delete the cookie with the given name. This command will be a no-op if there
+    is no such cookie visible to the current page.
+
+ Input: 1
+    Required:
+        STRING - name of cookie to delete
+
+ Usage:
+    $driver->delete_cookie_named('foo');
+
+=cut
+
+sub delete_cookie_named {
+    my ( $self, $cookie_name ) = @_;
+    if ( not defined $cookie_name ) {
+        croak "Cookie name not provided";
+    }
+    my $res = { 'command' => 'deleteCookieNamed', 'name' => $cookie_name };
+    return $self->_execute_command($res);
+}
+
+=head2 get_page_source
+
+ Description:
+    Get the current page source.
+
+ Output:
+    STRING - The page source.
+
+ Usage:
+    print $driver->get_page_source();
+
+=cut
+
+sub get_page_source {
+    my ($self) = @_;
+    my $res = { 'command' => 'getPageSource' };
+    return $self->_execute_command($res);
+}
+
+=head2 find_element
+
+ Description:
+    Search for an element on the page, starting from the document
+    root. The located element will be returned as a WebElement
+    object. If the element cannot be found, we will CROAK, killing
+    your script. If you wish for a warning instead, use the
+    parameterized version of the finders:
+
+        find_element_by_class
+        find_element_by_class_name
+        find_element_by_css
+        find_element_by_id
+        find_element_by_link
+        find_element_by_link_text
+        find_element_by_name
+        find_element_by_partial_link_text
+        find_element_by_tag_name
+        find_element_by_xpath
+
+    These functions all take a single STRING argument: the locator
+    search target of the element you want. If the element is found, we
+    will receive a WebElement. Otherwise, we will return 0. Note that
+    invoking methods on 0 will of course kill your script.
+
+ Input: 2 (1 optional)
+    Required:
+        STRING - The search target.
+    Optional:
+        STRING - Locator scheme to use to search the element, available schemes:
+                 {class, class_name, css, id, link, link_text, partial_link_text,
+                  tag_name, name, xpath}
+                 Defaults to 'xpath' if not configured global during instantiation.
+
+ Output:
+    Selenium::Remote::WebElement - WebElement Object
+        (This could be a subclass of L<Selenium::Remote::WebElement> if C<webelement_class> was set.
+
+ Usage:
+    $driver->find_element("//input[\@name='q']");
+
+=cut
+
+sub find_element {
+    my ( $self, $query, $method ) = @_;
+    if ( not defined $query ) {
+        croak 'Search string to find element not provided.';
+    }
+
+    my $res = { 'command' => 'findElement' };
+    my $params = $self->_build_find_params( $method, $query );
+    my $ret_data = eval { $self->_execute_command( $res, $params ); };
+    if ($@) {
+        if ( $@ =~
+/(An element could not be located on the page using the given search parameters)/
+          )
+        {
+            # give details on what element wasn't found
+            $@ = "$1: $query,$params->{using}";
+            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
+            croak $@;
+        }
+        else {
+            # re throw if the exception wasn't what we expected
+            die $@;
+        }
+    }
+    return $self->webelement_class->new(
+        id     => $ret_data,
+        driver => $self
+    );
+}
+
+=head2 find_elements
+
+ Description:
+    Search for multiple elements on the page, starting from the document root.
+    The located elements will be returned as an array of WebElement object.
+
+ Input: 2 (1 optional)
+    Required:
+        STRING - The search target.
+    Optional:
+        STRING - Locator scheme to use to search the element, available schemes:
+                 {class, class_name, css, id, link, link_text, partial_link_text,
+                  tag_name, name, xpath}
+                 Defaults to 'xpath' if not configured global during instantiation.
+
+ Output:
+    ARRAY or ARRAYREF of WebElement Objects
+
+ Usage:
+    $driver->find_elements("//input");
+
+=cut
+
+sub find_elements {
+    my ( $self, $query, $method ) = @_;
+    if ( not defined $query ) {
+        croak 'Search string to find element not provided.';
+    }
+
+    my $res = { 'command' => 'findElements' };
+    my $params = $self->_build_find_params( $method, $query );
+    my $ret_data = eval { $self->_execute_command( $res, $params ); };
+    if ($@) {
+        if ( $@ =~
+/(An element could not be located on the page using the given search parameters)/
+          )
+        {
+            # give details on what element wasn't found
+            $@ = "$1: $query,$params->{using}";
+            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
+            croak $@;
+        }
+        else {
+            # re throw if the exception wasn't what we expected
+            die $@;
+        }
+    }
+    my $elem_obj_arr = [];
+    foreach (@$ret_data) {
+        push(
+            @$elem_obj_arr,
+            $self->webelement_class->new(
+                id     => $_,
+                driver => $self
+            )
+        );
+    }
+    return wantarray ? @{$elem_obj_arr} : $elem_obj_arr;
+}
+
+=head2 find_child_element
+
+ Description:
+    Search for an element on the page, starting from the identified element. The
+    located element will be returned as a WebElement object.
+
+ Input: 3 (1 optional)
+    Required:
+        Selenium::Remote::WebElement - WebElement object from where you want to
+                                       start searching.
+        STRING - The search target. (Do not use a double whack('//')
+                 in an xpath to search for a child element
+                 ex: '//option[@id="something"]'
+                 instead use a dot whack ('./')
+                 ex: './option[@id="something"]')
+    Optional:
+        STRING - Locator scheme to use to search the element, available schemes:
+                 {class, class_name, css, id, link, link_text, partial_link_text,
+                  tag_name, name, xpath}
+                 Defaults to 'xpath' if not configured global during instantiation.
+
+ Output:
+    WebElement Object
+
+ Usage:
+    my $elem1 = $driver->find_element("//select[\@name='ned']");
+    # note the usage of ./ when searching for a child element instead of //
+    my $child = $driver->find_child_element($elem1, "./option[\@value='es_ar']");
+
+=cut
+
+sub find_child_element {
+    my ( $self, $elem, $query, $method ) = @_;
+    if ( ( not defined $elem ) || ( not defined $query ) ) {
+        croak "Missing parameters";
+    }
+    my $res = { 'command' => 'findChildElement', 'id' => $elem->{id} };
+    my $params = $self->_build_find_params( $method, $query );
+    my $ret_data = eval { $self->_execute_command( $res, $params ); };
+    if ($@) {
+        if ( $@ =~
+/(An element could not be located on the page using the given search parameters)/
+          )
+        {
+            # give details on what element wasn't found
+            $@ = "$1: $query,$params->{using}";
+            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
+            croak $@;
+        }
+        else {
+            # re throw if the exception wasn't what we expected
+            die $@;
+        }
+    }
+    return $self->webelement_class->new(
+        id     => $ret_data,
+        driver => $self
+    );
+}
+
+=head2 find_child_elements
+
+ Description:
+    Search for multiple element on the page, starting from the identified
+    element. The located elements will be returned as an array of WebElement
+    objects.
+
+ Input: 3 (1 optional)
+    Required:
+        Selenium::Remote::WebElement - WebElement object from where you want to
+                                       start searching.
+        STRING - The search target.
+    Optional:
+        STRING - Locator scheme to use to search the element, available schemes:
+                 {class, class_name, css, id, link, link_text, partial_link_text,
+                  tag_name, name, xpath}
+                 Defaults to 'xpath' if not configured global during instantiation.
+
+ Output:
+    ARRAY of WebElement Objects.
+
+ Usage:
+    my $elem1 = $driver->find_element("//select[\@name='ned']");
+    # note the usage of ./ when searching for a child element instead of //
+    my $child = $driver->find_child_elements($elem1, "./option");
+
+=cut
+
+sub find_child_elements {
+    my ( $self, $elem, $query, $method ) = @_;
+    if ( ( not defined $elem ) || ( not defined $query ) ) {
+        croak "Missing parameters";
+    }
+
+    my $res = { 'command' => 'findChildElements', 'id' => $elem->{id} };
+    my $params = $self->_build_find_params( $method, $query );
+    my $ret_data = eval { $self->_execute_command( $res, $params ); };
+    if ($@) {
+        if ( $@ =~
+/(An element could not be located on the page using the given search parameters)/
+          )
+        {
+            # give details on what element wasn't found
+            $@ = "$1: $query,$params->{using}";
+            local @CARP_NOT = ( "Selenium::Remote::Driver", @CARP_NOT );
+            croak $@;
+        }
+        else {
+            # re throw if the exception wasn't what we expected
+            die $@;
+        }
+    }
+    my $elem_obj_arr = [];
+    my $i            = 0;
+    foreach (@$ret_data) {
+        $elem_obj_arr->[$i] = $self->webelement_class->new(
+            id     => $_,
+            driver => $self
+        );
+        $i++;
+    }
+    return wantarray ? @{$elem_obj_arr} : $elem_obj_arr;
+}
+
+=head2 find_element_by_class
+
+See L</find_element>.
+
+=head2 find_element_by_class_name
+
+See L</find_element>.
+
+=head2 find_element_by_css
+
+See L</find_element>.
+
+=head2 find_element_by_id
+
+See L</find_element>.
+
+=head2 find_element_by_link
+
+See L</find_element>.
+
+=head2 find_element_by_link_text
+
+See L</find_element>.
+
+=head2 find_element_by_name
+
+See L</find_element>.
+
+=head2 find_element_by_partial_link_text
+
+See L</find_element>.
+
+=head2 find_element_by_tag_name
+
+See L</find_element>.
+
+=head2 find_element_by_xpath
+
+See L</find_element>.
+
+=head2 get_active_element
+
+ Description:
+    Get the element on the page that currently has focus.. The located element
+    will be returned as a WebElement object.
+
+ Output:
+    WebElement Object
+
+ Usage:
+    $driver->get_active_element();
+
+=cut
+
+sub _build_find_params {
+    my ( $self, $method, $query ) = @_;
+
+    my $using = $self->_build_using($method);
+
+    # geckodriver doesn't accept name as a valid selector
+    if ( $self->isa('Selenium::Firefox') && $using eq 'name' ) {
+        return {
+            using => 'css selector',
+            value => qq{[name="$query"]}
+        };
+    }
+    else {
+        return {
+            using => $using,
+            value => $query
+        };
+    }
+}
+
+sub _build_using {
+    my ( $self, $method ) = @_;
+
+    if ($method) {
+        if ( $self->FINDERS->{$method} ) {
+            return $self->FINDERS->{$method};
+        }
+        else {
+            croak 'Bad method, expected: '
+              . join( ', ', keys %{ $self->FINDERS } )
+              . ", got $method";
+        }
+    }
+    else {
+        return $self->default_finder;
+    }
+}
+
+sub get_active_element {
+    my ($self) = @_;
+    my $res = { 'command' => 'getActiveElement' };
+    my $ret_data = eval { $self->_execute_command($res) };
+    if ($@) {
+        croak $@;
+    }
+    else {
+        return $self->webelement_class->new(
+            id     => $ret_data,
+            driver => $self
+        );
+    }
+}
+
+=head2 cache_status
+
+ Description:
+    Get the status of the html5 application cache.
+
+ Usage:
+    print $driver->cache_status;
+
+ Output:
+    <number> - Status code for application cache: {UNCACHED = 0, IDLE = 1, CHECKING = 2, DOWNLOADING = 3, UPDATE_READY = 4, OBSOLETE = 5}
+
+=cut
+
+sub cache_status {
+    my ($self) = @_;
+    my $res = { 'command' => 'cacheStatus' };
+    return $self->_execute_command($res);
+}
+
+=head2 set_geolocation
+
+ Description:
+    Set the current geographic location - note that your driver must
+    implement this endpoint, or else it will crash your session. At the
+    very least, it works in v2.12 of Chromedriver.
+
+ Input:
+    Required:
+        HASH: A hash with key C<location> whose value is a Location hashref. See
+        usage section for example.
+
+ Usage:
+    $driver->set_geolocation( location => {
+        latitude  => 40.714353,
+        longitude => -74.005973,
+        altitude  => 0.056747
+    });
+
+ Output:
+    BOOLEAN - success or failure
+
+=cut
+
+sub set_geolocation {
+    my ( $self, %params ) = @_;
+    my $res = { 'command' => 'setGeolocation' };
+    return $self->_execute_command( $res, \%params );
+}
+
+=head2 get_geolocation
+
+ Description:
+    Get the current geographic location. Note that your webdriver must
+    implement this endpoint - otherwise, it will crash your session. At
+    the time of release, we couldn't get this to work on the desktop
+    FirefoxDriver or desktop Chromedriver.
+
+ Usage:
+    print $driver->get_geolocation;
+
+ Output:
+    { latitude: number, longitude: number, altitude: number } - The current geo location.
+
+=cut
+
+sub get_geolocation {
+    my ($self) = @_;
+    my $res = { 'command' => 'getGeolocation' };
+    return $self->_execute_command($res);
+}
+
+=head2 get_log
+
+ Description:
+    Get the log for a given log type. Log buffer is reset after each request.
+
+ Input:
+    Required:
+        <STRING> - Type of log to retrieve:
+        {client|driver|browser|server}. There may be others available; see
+        get_log_types for a full list for your driver.
+
+ Usage:
+    $driver->get_log( $log_type );
+
+ Output:
+    <ARRAY|ARRAYREF> - An array of log entries since the most recent request.
+
+=cut
+
+sub get_log {
+    my ( $self, $type ) = @_;
+    my $res = { 'command' => 'getLog' };
+    return $self->_execute_command( $res, { type => $type } );
+}
+
+=head2 get_log_types
+
+ Description:
+    Get available log types. By default, every driver should have client,
+    driver, browser, and server types, but there may be more available,
+    depending on your driver.
+
+ Usage:
+    my @types = $driver->get_log_types;
+    $driver->get_log($types[0]);
+
+ Output:
+    <ARRAYREF> - The list of log types.
+
+=cut
+
+sub get_log_types {
+    my ($self) = @_;
+    my $res = { 'command' => 'getLogTypes' };
+    return $self->_execute_command($res);
+}
+
+=head2 set_orientation
+
+ Description:
+    Set the browser orientation.
+
+ Input:
+    Required:
+        <STRING> - Orientation {LANDSCAPE|PORTRAIT}
+
+ Usage:
+    $driver->set_orientation( $orientation  );
+
+ Output:
+    BOOLEAN - success or failure
+
+=cut
+
+sub set_orientation {
+    my ( $self, $orientation ) = @_;
+    my $res = { 'command' => 'setOrientation' };
+    return $self->_execute_command( $res, { orientation => $orientation } );
+}
+
+=head2 get_orientation
+
+ Description:
+    Get the current browser orientation. Returns either LANDSCAPE|PORTRAIT.
+
+ Usage:
+    print $driver->get_orientation;
+
+ Output:
+    <STRING> - your orientation.
+
+=cut
+
+sub get_orientation {
+    my ($self) = @_;
+    my $res = { 'command' => 'getOrientation' };
+    return $self->_execute_command($res);
+}
+
+=head2 send_modifier
+
+ Description:
+    Send an event to the active element to depress or release a modifier key.
+
+ Input: 2
+    Required:
+      value - String - The modifier key event to be sent. This key must be one 'Ctrl','Shift','Alt',' or 'Command'/'Meta' as defined by the send keys command
+      isdown - Boolean/String - Whether to generate a key down or key up
+
+ Usage:
+    $driver->send_modifier('Alt','down');
+    $elem->send_keys('c');
+    $driver->send_modifier('Alt','up');
+
+    or
+
+    $driver->send_modifier('Alt',1);
+    $elem->send_keys('c');
+    $driver->send_modifier('Alt',0);
+
+=cut
+
+sub send_modifier {
+    my ( $self, $modifier, $isdown ) = @_;
+    if ( $isdown =~ /(down|up)/ ) {
+        $isdown = $isdown =~ /down/ ? 1 : 0;
+    }
+
+    if ( $self->{is_wd3}
+        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
+    {
+        my $acts = [
+            {
+                type => $isdown ? 'keyDown' : 'keyUp',
+                value => KEYS->{ lc($modifier) },
+            },
+        ];
+
+        my $action = {
+            actions => [
+                {
+                    id      => 'key',
+                    type    => 'key',
+                    actions => $acts,
+                }
+            ]
+        };
+        _queue_action(%$action);
+        return 1;
+    }
+
+    my $res = { 'command' => 'sendModifier' };
+    my $params = {
+        value  => $modifier,
+        isdown => $isdown
+    };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 compare_elements
+
+ Description:
+    Test if two element IDs refer to the same DOM element.
+
+ Input: 2
+    Required:
+        Selenium::Remote::WebElement - WebElement Object
+        Selenium::Remote::WebElement - WebElement Object
+
+ Output:
+    BOOLEAN
+
+ Usage:
+    $driver->compare_elements($elem_obj1, $elem_obj2);
+
+=cut
+
+sub compare_elements {
+    my ( $self, $elem1, $elem2 ) = @_;
+    my $res = {
+        'command' => 'elementEquals',
+        'id'      => $elem1->{id},
+        'other'   => $elem2->{id}
+    };
+    return $self->_execute_command($res);
+}
+
+=head2 click
+
+ Description:
+    Click any mouse button (at the coordinates set by the last moveto command).
+
+ Input:
+    button - any one of 'LEFT'/0 'MIDDLE'/1 'RIGHT'/2
+             defaults to 'LEFT'
+    queue - (optional) queue the click, rather than executing it.  WD3 only.
+
+ Usage:
+    $driver->click('LEFT');
+    $driver->click(1); #MIDDLE
+    $driver->click('RIGHT');
+    $driver->click;  #Defaults to left
+
+=cut
+
+sub click {
+    my ( $self, $button, $append ) = @_;
+    $button = _get_button($button);
+
+    my $res    = { 'command' => 'click' };
+    my $params = { 'button'  => $button };
+
+    if ( $self->{is_wd3}
+        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
+    {
+        $params = {
+            actions => [
+                {
+                    type       => "pointer",
+                    id         => 'mouse',
+                    parameters => { "pointerType" => "mouse" },
+                    actions    => [
+                        {
+                            type     => "pointerDown",
+                            duration => 0,
+                            button   => $button,
+                        },
+                        {
+                            type     => "pointerUp",
+                            duration => 0,
+                            button   => $button,
+                        },
+                    ],
+                }
+            ],
+        };
+        if ($append) {
+            _queue_action(%$params);
+            return 1;
+        }
+        return $self->general_action(%$params);
+    }
+
+    return $self->_execute_command( $res, $params );
+}
+
+sub _get_button {
+    my $button = shift;
+    my $button_enum = { LEFT => 0, MIDDLE => 1, RIGHT => 2 };
+    if ( defined $button && $button =~ /(LEFT|MIDDLE|RIGHT)/i ) {
+        return $button_enum->{ uc $1 };
+    }
+    if ( defined $button && $button =~ /(0|1|2)/ ) {
+        #Handle user error sending in "1"
+        return int($1);
+    }
+    return 0;
+}
+
+=head2 double_click
+
+ Description:
+    Double-clicks at the current mouse coordinates (set by moveto).
+
+ Compatibility:
+    On webdriver3 enabled servers, you can double click arbitrary mouse buttons.
+
+ Usage:
+    $driver->double_click(button);
+
+=cut
+
+sub double_click {
+    my ( $self, $button ) = @_;
+
+    $button = _get_button($button);
+
+    if ( $self->{is_wd3}
+        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
+    {
+        $self->click( $button, 1 );
+        $self->click( $button, 1 );
+        return $self->general_action();
+    }
+
+    my $res = { 'command' => 'doubleClick' };
+    return $self->_execute_command($res);
+}
+
+=head2 button_down
+
+ Description:
+    Click and hold the left mouse button (at the coordinates set by the
+    last moveto command). Note that the next mouse-related command that
+    should follow is buttonup . Any other mouse command (such as click
+    or another call to buttondown) will yield undefined behaviour.
+
+ Compatibility:
+    On WebDriver 3 enabled servers, all this does is queue a button down action.
+    You will either have to call general_action() to perform the queue, or an action like click() which also clears the queue.
+
+ Usage:
+    $self->button_down;
+
+=cut
+
+sub button_down {
+    my ($self) = @_;
+
+    if ( $self->{is_wd3}
+        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
+    {
+        my $params = {
+            actions => [
+                {
+                    type       => "pointer",
+                    id         => 'mouse',
+                    parameters => { "pointerType" => "mouse" },
+                    actions    => [
+                        {
+                            type     => "pointerDown",
+                            duration => 0,
+                            button   => 0,
+                        },
+                    ],
+                }
+            ],
+        };
+        _queue_action(%$params);
+        return 1;
+    }
+
+    my $res = { 'command' => 'buttonDown' };
+    return $self->_execute_command($res);
+}
+
+=head2 button_up
+
+ Description:
+    Releases the mouse button previously held (where the mouse is
+    currently at). Must be called once for every buttondown command
+    issued. See the note in click and buttondown about implications of
+    out-of-order commands.
+
+ Compatibility:
+    On WebDriver 3 enabled servers, all this does is queue a button down action.
+    You will either have to call general_action() to perform the queue, or an action like click() which also clears the queue.
+
+ Usage:
+    $self->button_up;
+
+=cut
+
+sub button_up {
+    my ($self) = @_;
+
+    if ( $self->{is_wd3}
+        && !( grep { $self->browser_name eq $_ } qw{MicrosoftEdge} ) )
+    {
+        my $params = {
+            actions => [
+                {
+                    type       => "pointer",
+                    id         => 'mouse',
+                    parameters => { "pointerType" => "mouse" },
+                    actions    => [
+                        {
+                            type     => "pointerDown",
+                            duration => 0,
+                            button   => 0,
+                        },
+                    ],
+                }
+            ],
+        };
+        _queue_action(%$params);
+        return 1;
+    }
+
+    my $res = { 'command' => 'buttonUp' };
+    return $self->_execute_command($res);
+}
+
+=head2 upload_file
+
+ Description:
+    Upload a file from the local machine to the selenium server
+    machine. That file then can be used for testing file upload on web
+    forms. Returns the remote-server's path to the file.
+
+    Passing raw data as an argument past the filename will upload
+    that rather than the file's contents.
+
+    When passing raw data, be advised that it expects a zipped
+    and then base64 encoded version of a single file.
+    Multiple files and/or directories are not supported by the remote server.
+
+ Usage:
+    my $remote_fname = $driver->upload_file( $fname );
+    my $element = $driver->find_element( '//input[@id="file"]' );
+    $element->send_keys( $remote_fname );
+
+=cut
+
+# this method duplicates upload() method in the
+# org.openqa.selenium.remote.RemoteWebElement java class.
+
+sub upload_file {
+    my ( $self, $filename, $raw_content ) = @_;
+
+    my $params;
+    if ( defined $raw_content ) {
+
+        #If no processing is passed, send the argument raw
+        $params = { file => $raw_content };
+    }
+    else {
+        #Otherwise, zip/base64 it.
+        $params = $self->_prepare_file($filename);
+    }
+
+    my $res = { 'command' => 'uploadFile' };    # /session/:SessionId/file
+    my $ret = $self->_execute_command( $res, $params );
+
+    return $ret;
+}
+
+sub _prepare_file {
+    my ( $self, $filename ) = @_;
+
+    if ( not -r $filename ) { croak "upload_file: no such file: $filename"; }
+    my $string = "";                            # buffer
+    my $zip    = Archive::Zip->new();
+    $zip->addFile( $filename, basename($filename) );
+    if ( $zip->writeToFileHandle( IO::String->new($string) ) != AZ_OK ) {
+        die 'zip failed';
+    }
+
+    return { file => MIME::Base64::encode_base64( $string, '' ) };
+}
+
+=head2 get_text
+
+ Description:
+    Get the text of a particular element. Wrapper around L</find_element>
+
+ Usage:
+    $text = $driver->get_text("//div[\@name='q']");
+
+=cut
+
+sub get_text {
+    my $self = shift;
+    return $self->find_element(@_)->get_text();
+}
+
+=head2 get_body
+
+ Description:
+    Get the current text for the whole body. If you want the entire raw HTML instead,
+    See L</get_page_source>.
+
+ Usage:
+    $body_text = $driver->get_body();
+
+=cut
+
+sub get_body {
+    my $self = shift;
+    return $self->get_text( '//body', 'xpath' );
+}
+
+=head2 get_path
+
+ Description:
+     Get the path part of the current browser location.
+
+ Usage:
+     $path = $driver->get_path();
+
+=cut
+
+sub get_path {
+    my $self     = shift;
+    my $location = $self->get_current_url;
+    $location =~ s/\?.*//;               # strip of query params
+    $location =~ s/#.*//;                # strip of anchors
+    $location =~ s#^https?://[^/]+##;    # strip off host
+    return $location;
+}
+
+=head2 get_user_agent
+
+ Description:
+    Convenience method to get the user agent string, according to the
+    browser's value for window.navigator.userAgent.
+
+ Usage:
+    $user_agent = $driver->get_user_agent()
+
+=cut
+
+sub get_user_agent {
+    my $self = shift;
+    return $self->execute_script('return window.navigator.userAgent;');
+}
+
+=head2 set_inner_window_size
+
+ Description:
+     Set the inner window size by closing the current window and
+     reopening the current page in a new window. This can be useful
+     when using browsers to mock as mobile devices.
+
+     This sub will be fired automatically if you set the
+     C<inner_window_size> hash key option during instantiation.
+
+ Input:
+     INT - height of the window
+     INT - width of the window
+
+ Output:
+     BOOLEAN - Success or failure
+
+ Usage:
+     $driver->set_inner_window_size(640, 480)
+
+=cut
+
+sub set_inner_window_size {
+    my $self     = shift;
+    my $height   = shift;
+    my $width    = shift;
+    my $location = $self->get_current_url;
+
+    $self->execute_script( 'window.open("' . $location . '", "_blank")' );
+    $self->close;
+    my @handles = @{ $self->get_window_handles };
+    $self->switch_to_window( pop @handles );
+
+    my @resize = (
+        'window.innerHeight = ' . $height,
+        'window.innerWidth  = ' . $width,
+        'return 1'
+    );
+
+    return $self->execute_script( join( ';', @resize ) ) ? 1 : 0;
+}
+
+=head2 get_local_storage_item
+
+ Description:
+     Get the value of a local storage item specified by the given key.
+
+ Input: 1
+    Required:
+        STRING - name of the key to be retrieved
+
+ Output:
+     STRING - value of the local storage item
+
+ Usage:
+     $driver->get_local_storage_item('key')
+
+=cut
+
+sub get_local_storage_item {
+    my ( $self, $key ) = @_;
+    my $res    = { 'command' => 'getLocalStorageItem' };
+    my $params = { 'key'     => $key };
+    return $self->_execute_command( $res, $params );
+}
+
+=head2 delete_local_storage_item
+
+ Description:
+     Get the value of a local storage item specified by the given key.
+
+ Input: 1
+    Required
+        STRING - name of the key to be deleted
+
+ Usage:
+     $driver->delete_local_storage_item('key')
+
+=cut
+
+sub delete_local_storage_item {
+    my ( $self, $key ) = @_;
+    my $res    = { 'command' => 'deleteLocalStorageItem' };
+    my $params = { 'key'     => $key };
+    return $self->_execute_command( $res, $params );
+}
+
+sub _coerce_timeout_ms {
+    my ($ms) = @_;
+
+    if ( defined $ms ) {
+        return _coerce_number($ms);
+    }
+    else {
+        croak 'Expecting a timeout in ms';
+    }
+}
+
+sub _coerce_number {
+    my ($maybe_number) = @_;
+
+    if ( Scalar::Util::looks_like_number($maybe_number) ) {
+        return $maybe_number + 0;
+    }
+    else {
+        croak "Expecting a number, not: $maybe_number";
+    }
+}
+
+1;

+ 5 - 5
lib/Selenium/Remote/Driver/v4.pm

@@ -59,7 +59,6 @@ our %cmd_map = (
     'GetAlertText' => 'get_alert_text',
     'SendAlertText' => 'send_keys_to_prompt',
     'TakeScreenshot' => 'screenshot',
-    'TakeElementScreenshot',
 );
 
 my @element_methods = (
@@ -76,7 +75,8 @@ my @element_methods = (
     'ElementClick',
     'ElementClear',
     'ElementSendKeys',
-)
+    'TakeElementScreenshot',
+);
 
 my @unimplemented = qw{
     new_desired_session
@@ -94,7 +94,7 @@ my @unimplemented = qw{
 
 sub _install_wrapper_subs {
     my ( $self ) = @_;
-    foreach my $sub_per_spec keys(%{$self->{client}{spec}}) {
+    foreach my $sub_per_spec (keys(%{$self->{client}{spec}})) {
         my $sub2install = $cmd_map{$sub_per_spec};
         if( !$sub2install ) {
             print "Can't install a subroutine to match '$sub_per_spec', as it isn't mapped to the S::R::D equivalent!" if $self->{'debug'};
@@ -106,13 +106,13 @@ sub _install_wrapper_subs {
             {
                 code => sub {
                     my $self = shift;
-                    my $sub_from_client = $self->{'_client'}->can($sub);
+                    my $sub_from_client = $self->{'_client'}->can($sub_per_spec);
                     return $sub_from_client->( $self->{'_client'}, @_ );
                 },
                 as   => $sub2install,
                 into => "Selenium::Remote::Driver::v4",
             }
-        ) unless "Selenium::Remote::Driver::v4"->can($sub);
+        ) unless "Selenium::Remote::Driver::v4"->can($sub2install);
     }
     foreach my $die_on (@unimplemented) {
         Sub::Install::install_sub(