Sfoglia il codice sorgente

Fix #345: Implement WC3 webdriver standard compatibility

This adds a new module, Selenium::Remote::Spec that describes
the new specification, along with processing methods to simplify
shims needed in multiple methods.

Current Firefox, Chrome and Edge are verified as working, and
have a variety of tweaks to ensure as many methods as possible
work as well (or better) than they previously did.

More work will need to be done in the future to complete the
JSONWire protocol, so we can both fall back when legacy methods
are available and also be more useful to users in general.

An acceptance testing directory (at) has been added with tests
designed to be run against the various drivers directly by
module contributors and authors.  The idea here is that we can
keep an eye on what works and does not via usage of these tests.
George S. Baugh 8 anni fa
parent
commit
3f61691755

+ 1 - 0
.gitignore

@@ -8,6 +8,7 @@ cover_db
 *.sublime*
 .build
 Selenium-Remote-Driver-*
+selenium*.jar
 .prove
 _prove
 #vim swapfiles

+ 1 - 3
.travis.yml

@@ -6,8 +6,6 @@ perl:
    - '5.18'
    - '5.16'
    - '5.14'
-   - '5.12'
-   - '5.10'
 matrix:
    fast_finish: true
    include:
@@ -24,7 +22,7 @@ install:
    - cpan-install --coverage   # installs converage prereqs, if enabled
    - cpanm --quiet --notest Devel::Cover::Report::Coveralls #send to coveralls
    - cpanm --quiet --notest Dist::Zilla::App::Command::cover #make sure we can dzil cover
-   - cpanm --quiet --notest --skip-satisfied Dist::Zilla
+   - cpanm --quiet --notest --skip-satisfied Dist::Zilla #unfortunately, we need a very new (6.0 or better) dzil, so no perl < 5.14
    - cpanm --quiet --notest --skip-satisfied Test::Spec WWW::Mechanize Test::WWW::Selenium #Test::Pod::Coverage can be stupid
    - "dzil authordeps          --missing | grep -vP '[^\\w:]' | xargs -n 5 -P 10 cpanm --quiet --notest"
    - "dzil listdeps   --author --missing | grep -vP '[^\\w:]' | xargs -n 5 -P 10 cpanm --quiet --notest"

+ 215 - 0
at/legacy.test

@@ -0,0 +1,215 @@
+use strict;
+use warnings;
+
+use Cwd qw{abs_path};
+use FindBin;
+
+use Test::More;
+use Test::Fatal;
+use Test::Deep;
+
+use Selenium::Remote::Driver;
+use Selenium::Remote::WDKeys;
+
+my $driver = Selenium::Remote::Driver->new(
+    remote_server_addr => '10.17.64.252',
+    port => 4444,
+    browser_name => 'firefox',
+    accept_ssl_certs => 1,
+);
+isa_ok($driver,'Selenium::Remote::Driver',"Can get new S::R::D with WebDriver3");
+
+$driver->debug_on();
+
+is($driver->get_capabilities()->{browserName},'firefox',"Can get Capabilities correctly (WD2)");
+my $sessions = $driver->get_sessions();
+is(scalar(@$sessions),1,"Can fall back to selenium2 to list sessions");
+
+ok($driver->status()->{build}->{version},"status reports OK (WD2)");
+
+#TODO do something about available_engines
+
+is( exception { $driver->set_timeout('page load',10000) },  undef, "WD2 set_timeout pageload OK");
+is( exception { $driver->set_timeout('script',10000) },     undef, "WD2 set_timeout script OK");
+is( exception { $driver->set_timeout('implicit',10000) },   undef, "WD2 set_timeout implicit OK");
+is( exception { $driver->set_async_script_timeout(20000) }, undef, "WD2 set_async_script_timeout OK");
+is( exception { $driver->set_implicit_wait_timeout(5000) }, undef, "WD2 set_implicit_wait_timeout OK");
+
+my $loc = abs_path("$FindBin::Bin/test.html");
+my $local_loc = $driver->upload_file($loc);
+note $local_loc;
+
+ok($driver->get("file://$local_loc"),"Can load a web page (WD2)");
+
+is($driver->get_alert_text(),"BEEE DOOO","get_alert_text works (WD2)");
+is(exception { $driver->dismiss_alert() }, undef, "alert can be dismissed (WD2)");
+
+#This sucker wants "value" instead of "text" like in legacy
+ok($driver->send_keys_to_prompt("HORGLE"),"send_keys_to_prompt works (WD2)");
+is(exception { $driver->accept_alert() }, undef, "alert can be accepted (WD2)");
+
+my $handle = $driver->get_current_window_handle();
+ok($handle,"Got a window handle (WD2)");
+cmp_bag($driver->get_window_handles(),[$handle],"Can list window handles (WD2)");
+
+my $sz = $driver->get_window_size();
+ok(defined $sz->{height},"get_window_size works (WD2)");
+ok(defined $sz->{width},"get window size works (WD2)");
+my $pos = $driver->get_window_position();
+ok(defined $pos->{x},"get_window_size works (WD2)");
+ok(defined $pos->{y},"get window size works (WD2)");
+
+like($driver->get_current_url(),qr/test.html$/,"get_current_url works (WD2)");
+like($driver->get_title(),qr/test/i,"get_title works (WD2)");
+
+my $otherloc = abs_path("$FindBin::Bin/other.html");
+my $other_local_loc = $driver->upload_file($otherloc);
+note $other_local_loc;
+
+$driver->get("file://$other_local_loc");
+$driver->go_back();
+$driver->dismiss_alert();
+$driver->dismiss_alert();
+like($driver->get_title(),qr/test/i,"go_back works (WD2)");
+
+$driver->go_forward();
+like($driver->get_page_source(),qr/ZIPPY/,"go_forward & get_page_source works (WD2)");
+is(exception { $driver->refresh() }, undef, "refresh works (WD2)");
+$driver->go_back();
+$driver->dismiss_alert();
+$driver->dismiss_alert();
+
+#TODO execute_*_script testing
+
+ok($driver->screenshot(),"can get base64'd whole page screenshot (WD2)");
+
+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($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 (WD2)");
+
+isa_ok($driver->get_active_element(),"Selenium::Remote::WebElement");
+
+TODO: {
+    local $TODO = "These methods aren't supported on firefox";
+    is(exception { $driver->cache_status() },undef, "cache_status works in WD2");
+    is(exception {
+        $driver->set_geolocation(location => {
+               latitude  => 40.714353,
+               longitude => -74.005973,
+               altitude  => 0.056747
+        });
+    }, undef, "set_geolocation works in WD2");
+    is(exception { $driver->get_geolocation() }, undef, "get_geolocation works in WD2");
+    is(exception { $driver->set_orientation("LANDSCAPE") }, undef, "set_orientation works in WD2");
+    is(exception { $driver->get_orientation() }, undef, "get_orientation works in WD2");
+}
+
+ok($driver->get_log('server'), "get_log fallback works");
+ok( scalar(@{$driver->get_log_types()}),"can fallback for get_log_types");
+
+like($driver->upload_file($otherloc),qr/other.html$/,"upload_file fallback works");
+
+#Jinkies, this stuff is cool, it prints the selenium server help page @_@
+#diag explain $driver->get_local_storage_item('whee');
+#diag explain $driver->delete_local_storage_item('whee');
+
+ok($driver->switch_to_frame($driver->find_element('frame', 'id')),"can switch to frame (WD2)");
+ok($driver->switch_to_frame(),"can switch to parent frame (WD2 only)");
+
+ok($driver->set_window_position(1,1),"can set window position (WD2)");
+ok($driver->set_window_size(200,200),"can set window size (WD2)");
+
+ok($driver->maximize_window(),"can maximize window (WD2)");
+
+is(scalar(@{$driver->get_all_cookies()}),1,"can get cookie list (WD2)");
+$driver->delete_all_cookies();
+is(scalar(@{$driver->get_all_cookies()}),0,"can delete all cookies (WD2)");
+
+ok($driver->mouse_move_to_location( element => $driver->find_element('a','tag_name')),"Can use mouse_move_to_location");
+$driver->click();
+my $handles = $driver->get_window_handles();
+is(scalar(@$handles),2,"Can move to element and then click it correctly (WD2)");
+
+$driver->switch_to_window($handles->[1]);
+is(exception { $driver->close() }, undef, "Can close new window (WD2)");
+cmp_bag($driver->get_window_handles,[$handles->[0]],"Correct window closed (WD2)");
+$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
+SKIP: {
+    skip("P.sure send_modifier is kind of screwed up",1);
+    $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() works (WD2)");
+is($input->get_attribute('value',1),'defaulthowdyeee',"element->get_attribute() second arg ignored (WD2)");
+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 (WD2)");
+$input->clear();
+is($input->get_attribute('value'),'',"clear() works (WD2)");
+
+is(exception { $driver->button_down() },undef,"Can button down (WD2)");
+is(exception { $driver->button_up() },undef,"Can button up (WD2)");
+
+ok($driver->find_element('radio2','id')->is_selected(),"WD2 is_selected() works");
+my $l1 = $driver->find_element('radio1','id');
+SKIP: {
+    skip "set_selected, toggle_element, is_selected looks broke", 1;
+    $l1->set_selected();
+    $l1->set_selected();
+    ok($l1->is_selected(),"WD2 set_selected works");
+    $l1->toggle();
+    ok(!$l1->is_selected(),"WD2 toggle works: off");
+    $l1->toggle();
+    ok($l1->is_selected(),"WD2 toggle works: on");
+}
+
+my $l2 = $driver->find_element('hammertime','id');
+is( $l2->is_enabled(),0,"is_enabled works (WD2)");
+ok( $l2->get_element_location()->{x},"Can get element rect (WD2)");
+ok( $l2->get_size()->{'height'}, "Size shim on rect works (WD2)");
+is( $l2->get_tag_name(),'input',"get_tag_name works (WD2)");
+is( exception { $l2->get_element_location_in_view() }, undef, "get_element_location_in_view not available in WD2");
+
+is($driver->find_element('hidon','id')->is_displayed(),0,"is_displayed returns false for type=hidden elements");
+is($driver->find_element('no-see-em','id')->is_displayed(),0,"is_displayed returns false for display=none");
+is($driver->find_element('h1','tag_name')->get_text(),'Howdy Howdy Howdy', "get_text works (WD2)");
+
+$driver->find_element('clickme','id')->click();
+is(exception { $driver->dismiss_alert() }, undef, "Can click element (WD2)");
+
+$driver->find_element('form','tag_name')->submit();
+like($driver->get_page_source(),qr/File not found/,"elem submit() works (WD2)");
+
+#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 (WD2)");
+
+$driver->delete_cookie_named('foo');
+is(scalar(@{$driver->get_all_cookies()}),0,"can delete named cookies (WD2)");
+
+is(exception { $driver->quit() }, undef, "Can quit (WD2)");
+
+done_testing();

+ 1 - 0
at/other.html

@@ -0,0 +1 @@
+ZIPPY

+ 228 - 0
at/sanity-chrome.test

@@ -0,0 +1,228 @@
+use strict;
+use warnings;
+
+use Cwd qw{abs_path};
+use FindBin;
+
+use Test::More;
+use Test::Fatal;
+use Test::Deep;
+
+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 => 'chrome',
+    accept_ssl_certs => 1,
+);
+isa_ok($driver,'Selenium::Remote::Driver',"Can get new S::R::D with WebDriver3");
+
+$driver->debug_on();
+
+is($driver->get_capabilities()->{browserName},'chrome',"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();
+$driver->dismiss_alert();
+$driver->dismiss_alert();
+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();
+$driver->dismiss_alert();
+$driver->dismiss_alert();
+
+#TODO execute_*_script testing
+
+ok($driver->screenshot(),"can get base64'd whole page screenshot (WD3)");
+SKIP: {
+    skip "chromedriver doesn't know how to take element screenshots", 1;
+    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($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)");
+
+isa_ok($driver->get_active_element(),"Selenium::Remote::WebElement");
+
+is(exception { $driver->cache_status() },undef, "cache_status implemented in krom");
+my $coords = {
+   latitude  => 40.714353,
+   longitude => -74.005973,
+};
+ok($driver->set_geolocation(location => $coords),"can set_geolocation in krom");
+is( $driver->get_geolocation()->{latitude},$coords->{latitude},"cang get_geolocation in krom");
+
+ok($driver->get_log('server'), "get_log fallback works");
+ok( scalar(@{$driver->get_log_types()}),"can fallback for get_log_types");
+
+isnt(exception { $driver->set_orientation("LANDSCAPE") },undef,"set_orientation unavailable on desktop");
+like(exception { $driver->get_orientation() },qr/error/,"get_orientation unavailable on desktop");
+
+like($driver->upload_file($otherloc),qr/other.html$/,"upload_file fallback works");
+
+#Jinkies, this stuff is cool, it prints the selenium server help page @_@
+#diag explain $driver->get_local_storage_item('whee');
+#diag explain $driver->delete_local_storage_item('whee');
+
+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(200,200),"can set window size (WD3)");
+
+SKIP: {
+    skip "chromedriver does not maximize/minimize", 2;
+    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)");
+
+#XXX chrome has issued a fatwah against cookies issued by file:// urls
+is(scalar(@{$driver->get_all_cookies()}),0,"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();
+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();
+isnt(exception {$driver->send_modifier('Shift','down')}, undef, "chromedriver can't send_modifier");
+$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)");
+TODO: {
+    local $TODO = "chrome can't get_property()";
+    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)");
+SKIP: {
+    skip "chrome don't actionChain", 1;
+    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();
+TODO: {
+    local $TODO = "chrome toggle won't allow bogus values (radios with both disabled)";
+    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)");
+is( exception { $l2->get_element_location_in_view() }, undef, "get_element_location_in_view available in gegl krom");
+
+is($driver->find_element('hidon','id')->is_displayed(),0,"is_displayed returns false for type=hidden elements");
+is($driver->find_element('no-see-em','id')->is_displayed(),0,"is_displayed returns false for display=none");
+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();

+ 246 - 0
at/sanity-edge.test

@@ -0,0 +1,246 @@
+use strict;
+use warnings;
+
+use Cwd qw{abs_path};
+use FindBin;
+
+use Test::More;
+use Test::Fatal;
+use Test::Deep;
+
+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 => 'MicrosoftEdge',
+    accept_ssl_certs => 1,
+);
+isa_ok($driver,'Selenium::Remote::Driver',"Can get new S::R::D with WebDriver3");
+
+$driver->debug_on();
+
+is($driver->get_capabilities()->{browserName},'MicrosoftEdge',"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);
+
+SKIP: {
+    skip "edge don't do get timeouts", 5;
+    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);
+    my $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
+SKIP: {
+    skip "send_keys_to_prompt is borkbork on edge", 1;
+    ok($driver->send_keys_to_prompt("HORGLE"),"send_keys_to_prompt works (WD3)");
+}
+sleep(5); #XXX edge bug
+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)");
+
+SKIP: {
+    skip "Can't do window rect on edge", 4;
+    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();
+sleep(5);
+$driver->dismiss_alert();
+sleep(5);
+$driver->dismiss_alert();
+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();
+sleep(5);
+$driver->dismiss_alert();
+sleep(5);
+$driver->dismiss_alert();
+
+#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($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)");
+
+isa_ok($driver->get_active_element(),"Selenium::Remote::WebElement");
+
+is(exception { $driver->cache_status() },undef, "cache_status works");
+is(exception {
+    $driver->set_geolocation(location => {
+       latitude  => 40.714353,
+       longitude => -74.005973,
+       altitude  => 0.056747
+});
+}, undef, "set_geolocation works");
+like(exception { $driver->get_geolocation() }, qr/permission to ask/i, "get_geolocation works");
+
+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/not implemented/i, "set_orientation unimplemented in WD3");
+like(exception { $driver->get_orientation() }, qr/not implemented/i, "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 @_@
+#diag explain $driver->get_local_storage_item('whee');
+#diag explain $driver->delete_local_storage_item('whee');
+
+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)");
+
+SKIP: {
+    skip "machete don't rect", 5;
+    ok($driver->set_window_position(1,1),"can set window position (WD3)");
+    ok($driver->set_window_size(200,200),"can set window size (WD3)");
+
+    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)");
+
+SKIP: {
+    skip 1, "delete_all_cookies Appears bork?", 1;
+    $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();
+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();
+SKIP: {
+    skip("send_modifier doesn't seem to work anywhere",1);
+    $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();
+
+is($input->get_attribute('value'),'defaulthowdyeee',"element->get_attribute() emulates old behavior thru get_property (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)");
+isnt(exception { $driver->release_general_action() }, undef, "Can't 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");
+TODO: {
+    local $TODO="Toggling elements doesn't work as advertised on Edge";
+    $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)");
+is( exception { $l2->get_element_location_in_view() }, undef, "get_element_location_in_view works");
+
+is($driver->find_element('hidon','id')->is_displayed(),0,"is_displayed returns false for type=hidden elements");
+is($driver->find_element('no-see-em','id')->is_displayed(),0,"is_displayed returns false for display=none");
+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)");
+
+SKIP: {
+    skip "edge doesn't like submitting disabled elements in forms", 1;
+    $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)");
+
+TODO: {
+    local $TODO= 'delete_cookie_named seems bork';
+    $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();

+ 217 - 0
at/sanity.test

@@ -0,0 +1,217 @@
+use strict;
+use warnings;
+
+use Cwd qw{abs_path};
+use FindBin;
+
+use Test::More;
+use Test::Fatal;
+use Test::Deep;
+
+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,
+);
+isa_ok($driver,'Selenium::Remote::Driver',"Can get new S::R::D with WebDriver3");
+
+$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();
+$driver->dismiss_alert();
+$driver->dismiss_alert();
+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();
+$driver->dismiss_alert();
+$driver->dismiss_alert();
+
+#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($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)");
+
+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 @_@
+#diag explain $driver->get_local_storage_item('whee');
+#diag explain $driver->delete_local_storage_item('whee');
+
+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(200,200),"can set window size (WD3)");
+
+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)");
+isnt( exception { $l2->get_element_location_in_view() }, undef, "get_element_location_in_view not available in WD3");
+
+is($driver->find_element('hidon','id')->is_displayed(),0,"is_displayed returns false for type=hidden elements");
+is($driver->find_element('no-see-em','id')->is_displayed(),0,"is_displayed returns false for display=none");
+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();

+ 39 - 0
at/test.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+    <head>
+         <meta charset="UTF-8" />
+        <title>Test Page</title>
+        <style>
+            .red {
+                color:red;
+            }
+        </style>
+        <script type="text/javascript">
+            window.onload = function() {
+                alert("BEEE DOOO");
+                prompt("Are you a fugitive from Justice?","Yes");
+            };
+        </script>
+        <meta http-equiv="set-cookie" content="GorgonGlaze=Petrified%20Grits; expires=Sat, 25-Nov-2120 12:00:00 GMT; path=/;" />
+    </head>
+    <body>
+        <h1>
+            Howdy Howdy Howdy
+        </h1>
+        <form id="howIsBabbyFormed" action="other.html">
+            <label for="text" class="red">Text</label>
+            <input name="text" type="text" value="default"></input>
+            <input id="radio1" name="radio2" type="radio"></input>
+            <input id="radio2" name="radio2" type="radio" checked></input>
+            <input id="hammertime" type="submit" disabled></input>
+            <input id="hidon" type="hidden"></input>
+        </form>
+        <br />
+        <button id="no-see-em" style="display:none;">Tickle</button>
+        <button id="clickme" onclick="alert('PARTY');">PARTY HARD</button>
+        <br />
+        <a href="other.html" target="_blank" id="linky" class="red">Test Link</a>
+        <br />
+        <iframe id="frame" src="other.html" />
+    </body>
+</html>

+ 34 - 7
lib/Selenium/Remote/Commands.pm

@@ -3,11 +3,13 @@ package Selenium::Remote::Commands;
 use strict;
 use warnings;
 
-# ABSTRACT: Implement commands for Selenium::Remote::Driver
+use Carp qw{croak};
+
+# ABSTRACT: Implement commands for Selenium::Remote::Driver for use with webdriver 2
 
 =head1 DESCRIPTION
 
-Defines all the HTTP endpoints available to execute on a selenium server.
+Defines all the HTTP endpoints available to execute on a selenium v2 server.
 
 If you have either a customized Selenium Server, or want new features
 you should update the _cmds hash.
@@ -358,11 +360,6 @@ has '_cmds' => (
                 'url'                => 'session/:sessionId/buttonup',
                 'no_content_success' => 1
             },
-            'generalAction' => {
-                'method'             => 'POST',
-                'url'                => 'session/:sessionId/actions',
-                'no_content_success' => 1
-            },
             'uploadFile' => {
                 'method'             => 'POST',
                 'url'                => 'session/:sessionId/file',
@@ -472,6 +469,10 @@ sub get_params {
     }
     my $data    = {};
     my $command = $args->{'command'};
+
+    #Allow fall-back in the event the command passed doesn't exist
+    return unless $self->get_cmds()->{$command};
+
     my $url     = $self->get_url($command);
 
     # Do the var substitutions.
@@ -489,6 +490,32 @@ sub get_params {
     return $data;
 }
 
+sub parse_response {
+    my ($self,$res,$resp) = @_;
+    if ( ref($resp) eq 'HASH' ) {
+        if ( $resp->{cmd_status} && $resp->{cmd_status} eq 'OK' ) {
+            return $resp->{cmd_return};
+        }
+        my $msg = "Error while executing command";
+        $msg .= ": $resp->{cmd_error}" if $resp->{cmd_error};
+        if ( $resp->{cmd_return} ) {
+            if ( ref( $resp->{cmd_return} ) eq 'HASH' ) {
+                $msg .= ": $res->{command}"
+                  if $res->{command};
+                $msg .= ": $resp->{cmd_return}->{error}->{msg}"
+                  if $resp->{cmd_return}->{error}->{msg};
+                $msg .= ": $resp->{cmd_return}->{message}"
+                  if $resp->{cmd_return}->{message};
+            }
+            else {
+                $msg .= ": $resp->{cmd_return}";
+            }
+        }
+        croak $msg;
+    }
+    return $resp;
+}
+
 1;
 
 __END__

File diff suppressed because it is too large
+ 546 - 108
lib/Selenium/Remote/Driver.pm


+ 17 - 0
lib/Selenium/Remote/RemoteConnection.pm

@@ -14,6 +14,7 @@ use Carp qw(croak);
 use JSON;
 use Data::Dumper;
 use Selenium::Remote::ErrorHandler;
+use Scalar::Util qw{looks_like_number};
 
 has 'remote_server_addr' => (
     is => 'rw',
@@ -146,6 +147,14 @@ sub request {
     }
 
     if ((defined $params) && $params ne '') {
+
+        #WebDriver 3 shims
+        if ($resource->{payload}) {
+            foreach my $key (keys(%{$resource->{payload}})) {
+                $params->{$key} = $resource->{payload}->{$key};
+            }
+        }
+
         my $json = JSON->new;
         $json->allow_blessed;
         $content = $json->allow_nonref->utf8->encode($params);
@@ -204,6 +213,14 @@ sub _process_response {
         elsif ($response->is_success) {
             $data->{'cmd_status'} = 'OK';
             if (defined $decoded_json) {
+
+                #XXX MS edge doesn't follow spec here either
+                if (looks_like_number($decoded_json->{status}) && $decoded_json->{status} > 0 && $decoded_json->{value}{message}) {
+                    $data->{cmd_status} = 'NOT OK';
+                    $data->{cmd_return} = $decoded_json->{value};
+                    return $data;
+                }
+
                 if ($no_content_success) {
                     $data->{'cmd_return'} = 1
                 }

+ 256 - 0
lib/Selenium/Remote/Spec.pm

@@ -0,0 +1,256 @@
+package Selenium::Remote::Spec;
+
+use strict;
+use warnings;
+
+# ABSTRACT: Implement commands for Selenium::Remote::Driver
+
+=head1 DESCRIPTION
+
+Defines all the HTTP endpoints available to execute on a selenium server.
+
+If you have either a customized Selenium Server, or want new features
+you should update the _cmds hash.
+
+=for Pod::Coverage *EVERYTHING*
+
+=cut
+
+use Carp qw{croak};
+use List::Util qw{any};
+
+use Moo;
+extends 'Selenium::Remote::Commands';
+
+#Ripped from the headlines: https://w3c.github.io/webdriver/webdriver-spec.html
+#then add 2 params for our use
+
+#Method    URI Template    no_content_success    internal_name    Command
+our $spec = qq{
+POST    session                                              0 newSession                   New Session
+POST    session                                              0 getCapabilities              Get Capabilities (v2->v3 shim)
+DELETE  session/:sessionId                                   1 quit                         Delete Session
+GET     status                                               0 status                       Status
+GET     session/:sessionId/timeouts                          0 getTimeouts                  Get Timeouts
+POST    session/:sessionId/timeouts                          1 setTimeout                   Set Page Load timeout (v2->v3 shim)
+POST    session/:sessionId/timeouts/async_script             1 setAsyncScriptTimeout        Set Async script timeout (v2->v3 shim)
+POST    session/:sessionId/timeouts/implicit_wait            1 setImplicitWaitTimeout       Set Implicit wait timeout (v2->v3 shim)
+POST    session/:sessionId/url                               1 get                          Navigate To
+GET     session/:sessionId/url                               0 getCurrentUrl                Get Current URL
+POST    session/:sessionId/back                              1 goBack                       Back
+POST    session/:sessionId/forward                           1 goForward                    Forward
+POST    session/:sessionId/refresh                           1 refresh                      Refresh
+GET     session/:sessionId/title                             0 getTitle Get                 Title
+GET     session/:sessionId/window                            0 getCurrentWindowHandle       Get Currently Focused Window Handle
+DELETE  session/:sessionId/window                            1 close                        Close Currently Focused Window
+POST    session/:sessionId/window                            1 switchToWindow               Switch To Window
+GET     session/:sessionId/window/handles                    0 getWindowHandles             Get Window Handles
+POST    session/:sessionId/frame                             1 switchToFrame                Switch To Frame
+POST    session/:sessionId/frame/parent                      1 switchToParentFrame          Switch To Parent Frame
+GET     session/:sessionId/window/rect                       0 getWindowSize                Get Window Size (v2->v3 shim)
+GET     session/:sessionId/window/rect                       0 getWindowPosition            Get Window Position (v2->v3 shim)
+POST    session/:sessionId/window/rect                       1 setWindowSize                Set Window Size (v2->v3 shim)
+POST    session/:sessionId/window/rect                       1 setWindowPosition            Set Window Position (v2->v3 shim)
+POST    session/:sessionId/window/maximize                   1 maximizeWindow               Maximize Window
+POST    session/:sessionId/window/minimize                   1 minimizeWindow               Minimize Window
+POST    session/:sessionId/window/fullscreen                 1 fullscreenWindow             Fullscreen Window
+GET     session/:sessionId/element/active                    0 getActiveElement             Get Active Element
+POST    session/:sessionId/element                           0 findElement                  Find Element
+POST    session/:sessionId/elements                          0 findElements                 Find Elements
+POST    session/:sessionId/element/:id/element               0 findChildElement             Find Element From Element
+POST    session/:sessionId/element/:id/elements              0 findChildElements            Find Elements From Element
+GET     session/:sessionId/element/:id/selected              0 isElementSelected            Is Element Selected
+GET     session/:sessionId/element/:id/attribute/:name       0 getElementAttribute          Get Element Attribute
+GET     session/:sessionId/element/:id/property/:name        0 getElementProperty           Get Element Property
+GET     session/:sessionId/element/:id/css/:propertyName     0 getElementValueOfCssProperty Get Element CSS Value
+GET     session/:sessionId/element/:id/text                  0 getElementText               Get Element Text
+GET     session/:sessionId/element/:id/name                  0 getElementTagName            Get Element Tag Name
+GET     session/:sessionId/element/:id/rect                  0 getElementRect               Get Element Rect
+GET     session/:sessionId/element/:id/enabled               0 isElementEnabled             Is Element Enabled
+POST    session/:sessionId/element/:id/click                 1 clickElement                 Element Click
+POST    session/:sessionId/element/:id/clear                 1 clearElement                 Element Clear
+POST    session/:sessionId/element/:id/value                 1 sendKeysToElement            Element Send Keys
+GET     session/:sessionId/source                            0 getPageSource                Get Page Source
+POST    session/:sessionId/execute/sync                      0 executeScript                Execute Script
+POST    session/:sessionId/execute/async                     0 executeAsyncScript           Execute Async Script
+GET     session/:sessionId/cookie                            0 getAllCookies                Get All Cookies
+GET     session/:sessionId/cookie/:name                      0 getCookieNamed               Get Named Cookie
+POST    session/:sessionId/cookie                            1 addCookie                    Add Cookie
+DELETE  session/:sessionId/cookie/:name                      1 deleteCookieNamed            Delete Cookie
+DELETE  session/:sessionId/cookie                            1 deleteAllCookies             Delete All Cookies
+POST    session/:sessionId/actions                           1 generalAction                Perform Actions
+DELETE  session/:sessionId/actions                           1 releaseGeneralAction         Release Actions
+POST    session/:sessionId/alert/dismiss                     1 dismissAlert                 Dismiss Alert
+POST    session/:sessionId/alert/accept                      1 acceptAlert                  Accept Alert
+GET     session/:sessionId/alert/text                        0 getAlertText                 Get Alert Text
+POST    session/:sessionId/alert/text                        1 sendKeysToPrompt             Send Alert Text
+GET     session/:sessionId/screenshot                        0 screenshot                   Take Screenshot
+GET     session/:sessionId/element/:id/screenshot            0 elementScreenshot            Take Element Screenshot
+};
+
+our $spec_parsed;
+
+sub get_spec {
+    return $spec_parsed if $spec_parsed;
+    my @split = split(/\n/,$spec);
+    foreach my $line (@split) {
+        next unless $line;
+        my ($method,$uri,$nc_success,$key,@description) =  split(/ +/,$line);
+        $spec_parsed->{$key} = {
+           method             => $method,
+           url                => $uri,
+           no_content_success => int($nc_success), #XXX this *should* always be 0, but specs lie
+           description        => join(' ',@description),
+        };
+    }
+    return $spec_parsed;
+}
+
+has '_cmds' => (
+    is      => 'lazy',
+    reader  => 'get_cmds',
+    builder => \&get_spec,
+);
+
+=head1 Webdriver 3 capabilities
+
+WD3 giveth and taketh away some caps.  Here's all you get:
+
+    Browser name:                     "browserName"             string  Identifies the user agent.
+    Browser version:                  "browserVersion"          string  Identifies the version of the user agent.
+    Platform name:                    "platformName"            string  Identifies the operating system of the endpoint node.
+    Accept insecure TLS certificates: "acceptInsecureCerts"     boolean Indicates whether untrusted and self-signed TLS certificates are implicitly trusted on navigation for the duration of the session.
+    Proxy configuration:              "proxy"                   JSON    Defines the current session’s proxy configuration.
+
+New Stuff:
+
+    Page load strategy:               "pageLoadStrategy"        string  Defines the current session’s page load strategy.
+    Window dimensioning/positioning:  "setWindowRect"           boolean Indicates whether the remote end supports all of the commands in Resizing and Positioning Windows.
+    Session timeouts configuration:   "timeouts"                JSON    Describes the timeouts imposed on certain session operations.
+    Unhandled prompt behavior:        "unhandledPromptBehavior" string  Describes the current session’s user prompt handler.
+
+=cut
+
+has '_caps' => (
+    is     => 'lazy',
+    reader => 'get_caps',
+    builder => sub {
+        return [
+            'browserName',
+            'acceptInsecureCerts',
+            'browserVersion',
+            'platformName',
+            'proxy',
+            'pageLoadStrategy',
+            'setWindowRect',
+            'timeouts',
+            'unhandledPromptBehavior',
+            'moz:firefoxOptions',
+            'chromeOptions',
+        ];
+    }
+);
+
+
+has '_caps_map' => (
+    is     => 'lazy',
+    reader => 'get_caps_map',
+    builder => sub {
+        return {
+            browserName    => 'browserName',
+            acceptSslCerts => 'acceptInsecureCerts',
+            version        => 'browserVersion',
+            platform       => 'platformName',
+            proxy          => 'proxy',
+        };
+    }
+);
+
+sub get_params {
+    my ( $self, $args ) = @_;
+    if ( !( defined $args->{'session_id'} ) ) {
+        return;
+    }
+
+    #Allow fall-back in the event the command passed doesn't exist
+    return unless $self->get_cmds()->{$args->{command}};
+
+    my $url     = $self->get_url($args->{command});
+
+    my $data = {};
+    # Do the var substitutions.
+    $url =~ s/:sessionId/$args->{'session_id'}/;
+    $url =~ s/:id/$args->{'id'}/;
+    $url =~ s/:name/$args->{'name'}/;
+    $url =~ s/:propertyName/$args->{'property_name'}/;
+    $url =~ s/:other/$args->{'other'}/;
+    $url =~ s/:windowHandle/$args->{'window_handle'}/;
+
+    $data->{'method'} = $self->get_method($args->{command});
+    $data->{'no_content_success'} = $self->get_no_content_success($args->{command});
+    $data->{'url'}    = $url;
+
+    #URL & data polyfills for the way selenium2 used to do things, etc
+    $data->{payload} = {};
+    if ($args->{type} ) {
+        $data->{payload}->{pageLoad} = $args->{ms} if $data->{url} =~ m/timeouts$/ && $args->{type} eq 'page load';
+        $data->{payload}->{script}   = $args->{ms} if $data->{url} =~ m/timeouts$/ && $args->{type} eq 'script';
+        $data->{payload}->{implicit} = $args->{ms} if $data->{url} =~ m/timeouts$/ && $args->{type} eq 'implicit';
+    }
+
+    #finder polyfills
+    #orig: class, class_name, css, id, link, link_text, partial_link_text, tag_name, name, xpath
+    #new:  "css selector", "link text", "partial link text", "tag name", "xpath"
+    #map: class, class_name, id, name, link = 'css selector'
+    if ($args->{using} && $args->{value}) {
+        $data->{payload}->{using} = 'css selector'            if grep {$args->{using} eq $_ } ('id', 'class name', 'name');
+        $data->{payload}->{value} = "#$args->{value}"         if $args->{using} eq 'id';
+        $data->{payload}->{value} = ".$args->{value}"         if $args->{using} eq 'class name';
+        $data->{payload}->{value} = "[name='$args->{value}']" if $args->{using} eq 'name';
+    }
+    if ($data->{url} =~ s/timeouts\/async_script$/timeouts/g) {
+        $data->{payload}->{script} = $args->{ms};
+        $data->{payload}->{type}   = 'script'; #XXX chrome doesn't follow the spec
+    }
+    if ( $data->{url} =~ s/timeouts\/implicit_wait$/timeouts/g) {
+        $data->{payload}->{implicit} = $args->{ms};
+        $data->{payload}->{type}     = 'implicit'; #XXX chrome doesn't follow the spec
+    }
+    $data->{payload}->{value}    = $args->{text}          if $args->{text} && $args->{command} ne 'sendKeysToElement';
+    $data->{payload}->{handle}   = $args->{window_handle} if grep { $args->{command} eq $_ } qw{setWindowSize getWindowSize setWindowPosition getWindowPosition fullscreenWindow minimizeWindow maximizeWindow};
+    return $data;
+}
+
+sub parse_response {
+    my ($self,undef,$resp) = @_;
+
+    if ( ref($resp) eq 'HASH' ) {
+        if ( $resp->{cmd_status} && $resp->{cmd_status} eq 'OK' ) {
+            return $resp->{cmd_return};
+        }
+        my $msg = "Error while executing command";
+        $msg .= ": $resp->{cmd_return}{error}"   if $resp->{cmd_return}{error};
+        $msg .= ": $resp->{cmd_return}{message}" if $resp->{cmd_return}{message};
+        croak $msg;
+    }
+
+    return $resp;
+}
+
+#Utility
+
+sub get_spec_differences {
+    my $v2_spec = Selenium::Remote::Commands->new()->get_cmds();
+    my $v3_spec = Selenium::Remote::Spec->new()->get_cmds();
+
+    foreach my $key (keys(%$v2_spec)) {
+        print "v2 $key NOT present in v3 spec!!!\n" unless any { $_ eq $key } keys(%$v3_spec);
+    }
+    foreach my $key (keys(%$v3_spec)) {
+        print "v3 $key NOT present in v2 spec!!!\n" unless any { $_ eq $key } keys(%$v2_spec);
+    }
+}
+
+1;
+
+__END__

+ 6 - 4
lib/Selenium/Remote/WDKeys.pm

@@ -33,10 +33,10 @@ use constant KEYS => {
     'page_down'  => "\N{U+E00f}",
     'end'    => "\N{U+E010}",
     'home'   => "\N{U+E011}",
-    'left_arrow'     => "\N{U+E012}",
+    'left_arrow' => "\N{U+E012}",
     'up_arrow'   => "\N{U+E013}",
-    'right_arrow'    => "\N{U+E014}",
-    'down_arrow'     => "\N{U+E015}",
+    'right_arrow'=> "\N{U+E014}",
+    'down_arrow' => "\N{U+E015}",
     'insert'     => "\N{U+E016}",
     'delete'     => "\N{U+E017}",
     'semicolon'  => "\N{U+E018}",
@@ -52,7 +52,7 @@ use constant KEYS => {
     'numpad_8'   => "\N{U+E022}",
     'numpad_9'   => "\N{U+E023}",
     'multiply'   => "\N{U+E024}",
-    'add'    => "\N{U+E025}",
+    'add'        => "\N{U+E025}",
     'separator'  => "\N{U+E026}",
     'subtract'   => "\N{U+E027}",
     'decimal'    => "\N{U+E028}",
@@ -70,6 +70,8 @@ use constant KEYS => {
     'f11'    => "\N{U+E03B}",
     'f12'    => "\N{U+E03C}",
     'command_meta'  => "\N{U+E03D}",
+    'ZenkakuHankaku' => "\N{U+E040}", #Asian language keys, maybe altGr too?
+    #There are other code points for say, left versus right meta/shift/alt etc, but I don't seriously believe anyone uses that level of sophistication on the web yet.
 };
 
 our @EXPORT = ('KEYS');

+ 178 - 27
lib/Selenium/Remote/WebElement.pm

@@ -140,6 +140,7 @@ sub click {
 
 sub submit {
     my ($self) = @_;
+    return $self->driver->execute_script("return arguments[0].submit();", {'element-6066-11e4-a52e-4f735466cecf'=> $self->{id}} ) if $self->driver->{is_wd3} && !(grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge});
     my $res = { 'command' => 'submitElement', 'id' => $self->id };
     return $self->_execute_command($res);
 }
@@ -203,6 +204,8 @@ sub send_keys {
 
 sub is_selected {
     my ($self) = @_;
+
+    return $self->get_property('checked') if $self->driver->{is_wd3} && !(grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge});
     my $res = { 'command' => 'isElementSelected', 'id' => $self->id };
     return $self->_execute_command($res);
 }
@@ -211,16 +214,19 @@ sub is_selected {
 
  Description:
     Select an OPTION element, or an INPUT element of type checkbox or radiobutton.
+    Forces selected=1 on the element..
 
  Usage:
     $elem->set_selected();
 
- Note: DEPRECATED -- use click instead
-
 =cut
 
 sub set_selected {
     my ($self) = @_;
+    if ($self->driver->{is_wd3}) {
+        return if $self->is_selected();
+        return $self->click();
+    }
     my $res = { 'command' => 'setElementSelected', 'id' => $self->id };
     return $self->_execute_command($res);
 }
@@ -237,12 +243,14 @@ sub set_selected {
  Usage:
     $elem->toggle();
 
- Note: DEPRECATED -- use click instead
-
 =cut
 
 sub toggle {
     my ($self) = @_;
+    if ($self->driver->{is_wd3}) {
+        return $self->click() unless $self->is_selected();
+        return $self->driver->execute_script(qq/ if (arguments[0].checked) { arguments[0].checked = 0 }; return arguments[0].checked; /, {'element-6066-11e4-a52e-4f735466cecf'=> $self->{id}});
+    }
     my $res = { 'command' => 'toggleElement', 'id' => $self->id };
     return $self->_execute_command($res);
 }
@@ -262,6 +270,7 @@ sub toggle {
 
 sub is_enabled {
     my ($self) = @_;
+    return $self->get_property('enabled') ? 1 : 0 if $self->driver->{is_wd3} && !(grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge});
     my $res = { 'command' => 'isElementEnabled', 'id' => $self->id };
     return $self->_execute_command($res);
 }
@@ -272,20 +281,79 @@ sub is_enabled {
    Determine an element's location on the page. The point (0, 0) refers to the
    upper-left corner of the page.
 
+ Compatibility:
+    On WebDriver 3 enabled servers, this is an alias for get_element_rect().
+
  Output:
     HASH - The X and Y coordinates for the element on the page.
 
  Usage:
     $elem->get_element_location();
 
+ This method is DEPRECATED on webdriver3 enabled servers.
+
 =cut
 
 sub get_element_location {
     my ($self) = @_;
+    if ($self->driver->{is_wd3} && !(grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge})) {
+        my $data = $self->get_element_rect();
+        delete $data->{height};
+        delete $data->{width};
+        return $data;
+    }
     my $res = { 'command' => 'getElementLocation', 'id' => $self->id };
     return $self->_execute_command($res);
 }
 
+=head2 get_size
+
+ Description:
+    Determine an element's size in pixels. The size will be returned with width
+    and height properties.
+
+ Compatibility:
+    On WebDriver 3 enabled servers, this is an alias for get_element_rect().
+
+ Output:
+    HASH - The width and height of the element, in pixels.
+
+ Usage:
+    $elem->get_size();
+
+ This method is DEPRECATED on webdriver3 enabled servers.
+
+=cut
+
+sub get_size {
+    my ($self) = @_;
+    if ($self->driver->{is_wd3} && !(grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge})) {
+        my $data = $self->get_element_rect();
+        delete $data->{x};
+        delete $data->{y};
+        return $data;
+    }
+    my $res = { 'command' => 'getElementSize', 'id' => $self->id };
+    return $self->_execute_command($res);
+}
+
+
+=head2 get_element_rect
+
+Get the element's size AND location in a hash.
+
+Example Output:
+
+    { x => 0, y => 0, height => 10, width => 10 }
+
+=cut
+
+sub get_element_rect {
+    my ($self) = @_;
+    my $res = { 'command' => 'getElementRect', 'id' => $self->id };
+    return $self->_execute_command($res);
+}
+
 =head2 get_element_location_in_view
 
  Description:
@@ -295,6 +363,9 @@ sub get_element_location {
     Note: This is considered an internal command and should only be used to
     determine an element's location for correctly generating native events.
 
+ Compatibility:
+    Not available on WebDriver3 enabled selenium servers.
+
  Output:
     {x:number, y:number} The X and Y coordinates for the element on the page.
 
@@ -349,23 +420,33 @@ sub clear {
  Description:
     Get the value of an element's attribute.
 
- Input: 1
+ Compatibility:
+    In older webDriver, this actually got the value of an element's property.
+    If you want to get the initial condition (e.g. the values in the tag hardcoded in HTML), pass 1 as the second argument.
+    This can only done on WebDriver 3 enabled servers.
+
+ Input: 2
     Required:
         STRING - name of the attribute of the element
+    Optional:
+        BOOLEAN - "I really mean that I want the initial condition, quit being so compatible!!!"
+
 
  Output:
     {STRING | NULL} The value of the attribute, or null if it is not set on the element.
 
  Usage:
-    $elem->get_attribute('name');
+    $elem->get_attribute('name',1);
 
 =cut
 
 sub get_attribute {
-    my ( $self, $attr_name ) = @_;
+    my ( $self, $attr_name, $no_i_really_mean_it ) = @_;
     if ( not defined $attr_name ) {
         croak 'Attribute name not provided';
     }
+    return $self->get_property($attr_name) if $self->driver->{is_wd3} && !(grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge}) && !$no_i_really_mean_it;
+
     my $res = {
         'command' => 'getElementAttribute',
         'id'      => $self->id,
@@ -374,6 +455,24 @@ sub get_attribute {
     return $self->_execute_command($res);
 }
 
+=head2 get_property
+
+Gets the C<Current Value> of an element's attribute.
+
+Takes a named property as an argument.
+
+Only available on WebDriver 3 enabled servers.
+
+=cut
+
+sub get_property {
+    my ($self,$prop) = @_;
+    return $self->get_attribute($prop) if $self->driver->{is_wd3} && (grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge});
+    my $res = { 'command' => 'getElementProperty', id => $self->id, name => $prop };
+    return $self->_execute_command($res);
+}
+
+
 =head2 get_value
 
  Description:
@@ -392,10 +491,13 @@ sub get_value {
     return $self->get_attribute('value');
 }
 
+=head2 get_style
+
 =head2 is_displayed
 
  Description:
     Determine if an element is currently displayed.
+    Note: This does *not* tell you an element's 'visibility' property; as it still takes up space in the DOM and is therefore considered 'displayed'.
 
  Output:
     BOOLEAN - Whether the element is displayed.
@@ -407,6 +509,10 @@ sub get_value {
 
 sub is_displayed {
     my ($self) = @_;
+    if ($self->driver->{is_wd3} && !(grep { $self->driver->browser_name eq $_ } qw{chrome MicrosoftEdge})) {
+        return 0 if $self->get_tag_name() eq 'input' && $self->get_property('type') eq 'hidden'; #hidden type inputs
+        return int($self->get_css_attribute('display') ne 'none');
+    }
     my $res = { 'command' => 'isElementDisplayed', 'id' => $self->id };
     return $self->_execute_command($res);
 }
@@ -464,26 +570,6 @@ sub drag {
     carp 'drag is no longer available in the JSONWireProtocol.';
 }
 
-=head2 get_size
-
- Description:
-    Determine an element's size in pixels. The size will be returned with width
-    and height properties.
-
- Output:
-    HASH - The width and height of the element, in pixels.
-
- Usage:
-    $elem->get_size();
-
-=cut
-
-sub get_size {
-    my ($self) = @_;
-    my $res = { 'command' => 'getElementSize', 'id' => $self->id };
-    return $self->_execute_command($res);
-}
-
 =head2 get_text
 
  Description:
@@ -546,6 +632,8 @@ sub get_css_attribute {
  Note: DEPRECATED as of 2.42.2 -- use get_text, get_value, is_displayed, or
  whatever appropriate WebElement function you need instead
 
+ Entirely unsupported on WebDriver 3 enabled servers.
+
 =cut
 
 sub describe {
@@ -554,4 +642,67 @@ sub describe {
     return $self->_execute_command($res);
 }
 
+=head2 screenshot
+
+ Description:
+    Get a screenshot of the visible region that is a subset of the element's bounding box as a base64 encoded image.
+
+ Compatibility:
+    Only available on Webdriver3 enabled selenium servers.
+
+ Input (optional):
+    $scroll_into_view - BOOLEAN default true.  If false, will not scroll the element into the viewport first.
+    Failing to do so may result in an image being cropped partially or entirely.
+
+ Output:
+    STRING - base64 encoded image
+
+ Usage:
+    print $element->screenshot();
+
+To conveniently write the screenshot to a file, see L</capture_screenshot>.
+
+=cut
+
+sub screenshot {
+    my ($self, $scroll) = @_;
+    $scroll //= 1;
+    my $res = { 'command' => 'elementScreenshot', id => $self->id };
+    my $input = {scroll => int($scroll) };
+    return $self->_execute_command($res, $input);
+}
+
+=head2 capture_screenshot
+
+ Description:
+    Capture a screenshot of said element and save as a PNG to provided file name.
+
+ Compatibility:
+    Only available on Webdriver3 enabled selenium servers.
+
+ Input (optional):
+    $scroll_into_view - BOOLEAN default true.  If false, will not scroll the element into the viewport first.
+    Failing to do so may result in an image being cropped partially or entirely.
+
+ Output:
+    TRUE - (Screenshot is written to file)
+
+ Usage:
+    $element->capture_screenshot($filename);
+
+=cut
+
+sub capture_screenshot {
+    my ( $self, $filename, $scroll ) = @_;
+    croak '$filename is required' unless $filename;
+
+    open( my $fh, '>', $filename );
+    binmode $fh;
+    print $fh MIME::Base64::decode_base64( $self->screenshot($scroll) );
+    CORE::close $fh;
+    return 1;
+}
+
+
+
 1;

+ 3 - 0
t/01-driver.t

@@ -17,6 +17,8 @@ use lib $FindBin::Bin . '/lib';
 use TestHarness;
 use Test::Fatal;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 my $harness = TestHarness->new(
     this_file => $FindBin::Script
 );
@@ -237,6 +239,7 @@ COOKIES: {
     pass('Deleting cookies...');
     $ret = $driver->get_all_cookies();
     is(@{$ret}, 0, 'Deleted all cookies.');
+    $driver->debug_on();
     $ret = $driver->add_cookie('foo', 'bar', '/', $domain, 0);
     pass('Adding cookie foo...');
     $ret = $driver->get_all_cookies();

+ 263 - 0
t/01-webdriver3.t

@@ -0,0 +1,263 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Deep;
+use Test::Fatal;
+use Scalar::Util qw{looks_like_number};
+
+use Selenium::Remote::Driver;
+use Selenium::Firefox::Profile;
+use Selenium::Remote::Spec;
+
+#So we only modify _request_new_session to get webd3 working.
+#As such, we should only test that.
+NEWSESS: {
+
+    #TODO cover case where ISA Selenium::Firefox
+    my $self = bless({ is_wd3 => 1 },"Selenium::Remote::Driver");
+    my $profile = Selenium::Firefox::Profile->new();
+    $profile->set_preference(
+        'browser.startup.homepage' => 'http://www.google.com',
+    );
+    my $args = {
+        desiredCapabilities => {
+            browserName       => 'firefox',
+            version            => 666,
+            platform           => 'ANY',
+            javascript         => 1,
+            acceptSslCerts     => 1,
+            firefox_profile    => $profile,
+            pageLoadStrategy   => 'none',
+            proxy => {
+                proxyType => 'direct',
+                proxyAutoconfigUrl => 'http://localhost',
+                ftpProxy           => 'localhost:1234',
+                httpProxy          => 'localhost:1234',
+                sslProxy           => 'localhost:1234',
+                socksProxy         => 'localhost:1234',
+                socksVersion       => 2,
+                noProxy            => ['http://localhost'],
+            },
+            extra_capabilities => { #TODO these need to be translated as moz:firefoxOptions => {} automatically, and then to be put in the main hash
+                binary  => '/usr/bin/firefox',
+                args    => ['-profile', '~/.mozilla/firefox/vbdgri9o.default'], #gotta check this gets overridden
+                profile => 'some Base64 string of a zip file. I should really make this a feature',
+                log     => 'trace', #trace|debug|config|info|warn|error|fatal
+                prefs   => {}, #TODO check that this is auto-set above by the Selenium::Firefox::Profile stuff
+                webdriverClick => 0, #This option is OP, *must* be set to false 24/7
+            },
+        },
+    };
+
+    no warnings qw{redefine once};
+    local *Selenium::Remote::RemoteConnection::request = sub {return { sessionId => 'zippy', cmd_status => 'OK', cmd_return => {capabilities => 'eee'} }};
+    local *File::Temp::Dir::dirname = sub { return '/tmp/zippy' };
+    use warnings;
+
+    my ($args_modified,undef) = $self->_request_new_session($args);
+
+    my $expected = {
+        'alwaysMatch' => {
+            'browserVersion'     => 666,
+            'moz:firefoxOptions' => {
+                'args' => [
+                    '-profile',
+                    '/tmp/zippy'
+                ],
+                'binary'  => '/usr/bin/firefox',
+                'log'     => 'trace',
+                'prefs'   => {},
+                'profile' => 'some Base64 string of a zip file. I should really make this a feature',
+                'webdriverClick' => 0
+            },
+            'platformName' => 'ANY',
+            'proxy'        => {
+                'ftpProxy'           => 'localhost:1234',
+                'httpProxy'          => 'localhost:1234',
+                'noProxy'            => [
+                    'http://localhost'
+                ],
+                'proxyAutoconfigUrl' => 'http://localhost',
+                'proxyType'          => 'direct',
+                'socksProxy'         => 'localhost:1234',
+                'socksVersion'       => 2,
+                'sslProxy'           => 'localhost:1234'
+            },
+            'browserName'      => 'firefox',
+            'pageLoadStrategy' => 'none',
+            acceptInsecureCerts => 1,
+        }
+    };
+
+    is($self->{capabilities},'eee',"Caps set correctly in wd3 mode");
+    is_deeply($args_modified->{capabilities},$expected,"Desired capabilities correctly translated to Firefox (WD3)");
+
+    $expected->{alwaysMatch}->{'chromeOptions'} = $expected->{alwaysMatch}->{'moz:firefoxOptions'};
+    $expected->{alwaysMatch}->{'moz:firefoxOptions'} = {};
+    $expected->{alwaysMatch}->{chromeOptions}->{args} = ['-profile', '~/.mozilla/firefox/vbdgri9o.default'];
+    $expected->{alwaysMatch}->{browserName} = 'chrome';
+
+    $args->{desiredCapabilities}->{browserName} = 'chrome';
+    ($args_modified,undef) = $self->_request_new_session($args);
+    is_deeply($args_modified->{capabilities},$expected,"Desired capabilities correctly translated to Krom (WD3)");
+
+}
+
+EXECOMMAND: {
+
+    #_execute_command with payload 'hitting all the right buttons'
+    #also check that fallback works w/ the right special missing word
+    #also check capability shortcut
+    my $self = bless({ is_wd3 => 1, capabilities => 'wakka wakka' },"Selenium::Remote::Driver");
+
+    no warnings qw{redefine once};
+    local *Selenium::Remote::RemoteConnection::request = sub {return { sessionId => 'zippy', cmd_status => 'OK' }};
+    local *Selenium::Remote::Spec::get_params          = sub { my ($self,$ret)       = @_; $ret->{v3} = 1; return $ret; };
+    local *Selenium::Remote::Commands::get_params      = sub { die 'whee' };
+    local *Selenium::Remote::Spec::parse_response      = sub { my ($self,undef,$ret) = @_; $ret->{rv3} = 1; return $ret; };
+    local *Selenium::Remote::Commands::parse_response  = sub { die 'zippy' };
+    use warnings;
+
+    my ($input,$params) = ({ command => 'zippy'},{ ms => 1, type=> 1, text => 1, value => 1, using => 1});
+
+    my $ret = $self->_execute_command($input,$params);
+    is($ret->{rv3},1,"v3 code walked in _execute_command on happy path");
+
+    $input->{command} = 'getCapabilities';
+    $ret = $self->_execute_command($input,$params);
+    is($ret,'wakka wakka',"v3 code walked in _execute_command on getCapabilities path");
+
+    $input->{command} = 'HORGLE';
+
+    no warnings qw{redefine once};
+    local *Selenium::Remote::Spec::get_params          = sub { return undef; };
+    local *Selenium::Remote::Commands::get_params      = sub { die 'whee' };
+    local *Selenium::Remote::Spec::parse_response      = sub { my ($self,undef,$ret) = @_; $ret->{rv3} = 1; return $ret; };
+    local *Selenium::Remote::Commands::parse_response  = sub { die 'zippy' };
+    use warnings;
+
+    $ret = exception { $self->_execute_command($input,$params) };
+    like($ret,qr/whee/,"v2 fallback walked in _execute_command on getCapabilities path");
+
+}
+
+REMOTECONN: {
+    my $self = bless({},'Selenium::Remote::RemoteConnection');
+    $self->remote_server_addr('eee');
+    $self->port(666);
+    no warnings qw{redefine once};
+    local *LWP::UserAgent::request = sub { my ($self,$req) = @_; return $req };
+    use warnings;
+
+    my $res = $self->request({ payload => { zippy => 1}, url => 'grid', method => 'eee' },{},1);
+    is($res->content,'{"zippy":1}',"RemoteConnection payload shim works");
+}
+
+#get_cmds, get_params, parse_response
+#get_caps and get_caps map have already been checked above in the _request_new_session code
+
+SPEC: {
+    my $obj = Selenium::Remote::Spec->new();
+    my $cmds = $obj->get_cmds();
+    subtest "parsing of spec blob done correctly" => sub {
+        foreach my $key (keys(%$cmds)) {
+            like($cmds->{$key}->{url},qr/^session|status/,"url parsed for $key correctly");
+            is($cmds->{$key}->{url},$obj->get_url($key),"get_url accessor works for $key");
+            like($cmds->{$key}->{method},qr/^GET|POST|DELETE|PUT$/,"method parsed for $key correctly");
+            is($cmds->{$key}->{method},$obj->get_method($key),"get_method accessor works for $key");
+            ok($cmds->{$key}->{description},"description parsed for $key correctly");
+            ok(looks_like_number($cmds->{$key}->{no_content_success}),"no_content_success parsed for $key correctly");
+            is($cmds->{$key}->{no_content_success},$obj->get_no_content_success($key),"get_no_content_success accessor works for $key");
+        }
+    };
+}
+
+SPEC_PARAMS: {
+    no warnings qw{redefine once};
+    local *Selenium::Remote::Spec::get_url = sub { return ':sessionId/:id/:name/:propertyName/:other/:windowHandle/timeouts' };
+    use warnings;
+
+    my $obj = Selenium::Remote::Spec->new();
+    my $args = {
+        session_id    => 'a',
+        id            => 'man',
+        name          => 'a',
+        property_name => 'plan',
+        other         => 'a canal',
+        window_handle => 'panama',
+        command       => 'setWindowSize',
+        ms            => 666,
+        type          => 'page load',
+        using         => 'id',
+        value         => 'whee',
+        text          => 'zippy',
+    };
+    my $expected = {
+        'method'             => 'POST',
+        'no_content_success' => 1,
+        'url'                => 'a/man/a/plan/a canal/panama/timeouts',
+        'payload'            => {
+            'handle'   => 'panama',
+            'pageLoad' => 666,
+            'using'    => 'css selector',
+            'value'    => 'zippy',
+        },
+    };
+    is_deeply($obj->get_params($args),$expected,"get_params: var substitution works, payload construction works (mostly)");
+
+    $args->{type} = 'implicit';
+    $expected->{payload}{implicit} = 666;
+    delete $expected->{payload}{pageLoad};
+    is_deeply($obj->get_params($args),$expected,"get_params: timeout payload mongling (implicit) works");
+
+    $args->{type} = 'script';
+    $expected->{payload}{script} = 666;
+    delete $expected->{payload}{implicit};
+    is_deeply($obj->get_params($args),$expected,"get_params: timeout payload mongling (script) works");
+
+    no warnings qw{redefine once};
+    local *Selenium::Remote::Spec::get_url = sub { return ':sessionId/:id/:name/:propertyName/:other/:windowHandle/timeouts/async_script' };
+    use warnings;
+
+    $args->{type} = 'page load';
+    delete $expected->{payload}{pageLoad};
+    $expected->{payload}{script} = 666;
+    $expected->{payload}{type} = 'script';
+    is_deeply($obj->get_params($args),$expected,"get_params: async_script substitution works");
+
+    no warnings qw{redefine once};
+    local *Selenium::Remote::Spec::get_url = sub { return ':sessionId/:id/:name/:propertyName/:other/:windowHandle/timeouts/implicit_wait' };
+    use warnings;
+
+    delete $expected->{payload}{script};
+    $expected->{payload}{implicit} = 666;
+    $expected->{payload}{type} = 'implicit';
+    is_deeply($obj->get_params($args),$expected,"get_params: implicit_wait substitution works");
+
+    delete $args->{text};
+    $expected->{payload}{value} = "#whee";
+    is_deeply($obj->get_params($args),$expected,"get_params: id css substitution works");
+
+    $args->{using} = 'class name';
+    $expected->{payload}{value} = ".whee";
+    is_deeply($obj->get_params($args),$expected,"get_params: class name css substitution works");
+
+    $args->{using} = 'name';
+    $expected->{payload}{value} = "[name='whee']";
+    is_deeply($obj->get_params($args),$expected,"get_params: name css substitution works");
+}
+
+PARSE_RESP: {
+    my $obj = Selenium::Remote::Spec->new();
+    my $expected = { error => 'ID10T', 'message' => 'Please insert another quarter'};
+    my $args = {
+        cmd_status => 'OK',
+        cmd_return => $expected,
+    };
+    is_deeply($obj->parse_response(undef,$args),$expected,"parse_response works");
+    $args->{cmd_status} = 'NOT OK';
+    like(exception { $obj->parse_response(undef,$args) },qr/insert another quarter/i,"parse_response throws on failure");
+}
+
+done_testing();

+ 2 - 0
t/02-webelement.t

@@ -9,6 +9,8 @@ use FindBin;
 use lib $FindBin::Bin . '/lib';
 use TestHarness;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 my $harness = TestHarness->new(
     this_file => $FindBin::Script
 );

+ 24 - 0
t/04-commands-implemented.t

@@ -3,6 +3,7 @@ use warnings;
 
 # TODO: find another way to do this checking, this is so fragile
 use Selenium::Remote::Commands;
+use Selenium::Remote::Spec;
 use Test::More;
 
 unless($ENV{RELEASE_TESTING}) {
@@ -31,6 +32,29 @@ for my $command (keys %{$comm}) {
   }
 }
 
+note "*************** WD3 methods ********************";
+$comm = Selenium::Remote::Spec->new->get_cmds;
+for my $command (keys %{$comm}) {
+  my $found_command = 0;
+  for my $file (
+    qw{lib/Selenium/Remote/Driver.pm
+    lib/Selenium/Remote/WebElement.pm
+    lib/Selenium/Firefox.pm}
+    ) {
+    open(my $fh, '<', $file) or die "Couldn't open file $file";
+    for (<$fh>) {
+      if (/'?command'?\s*=>\s*'$command'/
+       or /{'?commands'?}->\{'?$command'?}/) {
+        pass("find $command");
+        $found_command = 1;
+      }
+    }
+  }
+  if (!$found_command && $command !~ /Gecko/) {
+    fail("find $command");
+  }
+}
+
 done_testing;
 
 1;

+ 2 - 0
t/10-switch-to-window.t

@@ -15,6 +15,8 @@ my $harness = TestHarness->new(
 
 my @browsers = qw/chrome firefox/;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 foreach (@browsers) {
     my %selenium_args = (
         default_finder => 'css',

+ 2 - 1
t/11-action-chains.t

@@ -11,6 +11,8 @@ use FindBin;
 use lib $FindBin::Bin . '/lib';
 use TestHarness;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 my $harness = TestHarness->new(
     this_file => $FindBin::Script
 );
@@ -53,5 +55,4 @@ my %selenium_args = (
     $driver->quit;
 }
 
-
 done_testing;

+ 2 - 0
t/12-reuse-session.t

@@ -9,6 +9,8 @@ use FindBin;
 use lib $FindBin::Bin . '/lib';
 use TestHarness;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 my $harness = TestHarness->new(
     this_file => $FindBin::Script
 );

+ 2 - 0
t/Finders.t

@@ -6,6 +6,8 @@ use FindBin;
 use lib $FindBin::Bin . '/lib';
 use TestHarness;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 my $harness = TestHarness->new(
     this_file => $FindBin::Script
 );

+ 2 - 0
t/Firefox-Profile.t

@@ -15,6 +15,8 @@ use FindBin;
 use lib $FindBin::Bin . '/lib';
 use TestHarness;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 my $harness = TestHarness->new(
     this_file => $FindBin::Script
 );

+ 2 - 0
t/Test-Selenium-Remote-Driver-google.t

@@ -4,6 +4,8 @@ use Test::More;
 use Test::Selenium::Remote::Driver;
 use Selenium::Remote::Mock::RemoteConnection;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 use FindBin;
 use lib $FindBin::Bin . '/lib';
 use TestHarness;

+ 2 - 0
t/convenience.t

@@ -10,6 +10,8 @@ use Test::Selenium::InternetExplorer;
 use Test::Selenium::PhantomJS;
 use Test::More;
 
+$Selenium::Remote::Driver::FORCE_WD2 = 1;
+
 use FindBin;
 use lib $FindBin::Bin . '/lib';
 use TestHarness;

+ 3 - 0
tidyall.ini

@@ -0,0 +1,3 @@
+[PerlTidy]
+select = {lib,bin}/**/*
+argv = -noll -it=2

Some files were not shown because too many files changed in this diff