Explorar o código

Merge pull request #271 from gempesaw/geckodriver

Add compatibility with marionette (geckodriver) for firefox 48

- Fix #260
Daniel Gempesaw %!s(int64=9) %!d(string=hai) anos
pai
achega
d1745d76ae

+ 86 - 55
README.md

@@ -1,6 +1,4 @@
-# Selenium::Remote::Driver
-
-[![Build Status](https://travis-ci.org/gempesaw/Selenium-Remote-Driver.svg?branch=master)](https://travis-ci.org/gempesaw/Selenium-Remote-Driver)
+# Selenium::Remote::Driver [![Build Status](https://travis-ci.org/gempesaw/Selenium-Remote-Driver.svg?branch=master)](https://travis-ci.org/gempesaw/Selenium-Remote-Driver)
 
 [Selenium WebDriver][wd] is a test tool that allows you to write
 automated web application UI tests in any programming language against
@@ -8,11 +6,7 @@ any HTTP website using any mainstream JavaScript-enabled browser. This
 module is a Perl implementation of the client for the Webdriver
 [JSONWireProtocol that Selenium provides.][jsonwire]
 
-This module sends commands directly to the server using HTTP. Using
-this module together with the Selenium Server, you can automatically
-control any supported browser.
-
-[wd]: https://code.google.com/p/selenium/
+[wd]: http://www.seleniumhq.org/
 [jsonwire]: https://code.google.com/p/selenium/wiki/JsonWireProtocol
 [standalone]: http://selenium-release.storage.googleapis.com/index.html
 
@@ -24,26 +18,99 @@ It's probably easiest to use the `cpanm` or `CPAN` commands:
 $ cpanm Selenium::Remote::Driver
 ```
 
-If you want to install from this repository, you have a few options;
-see the [installation docs][] for more details.
+If you want to install from this repository, see the
+[installation docs][] for more details.
 
 [installation docs]: /INSTALL.md
 
 ## Usage
 
-You can either use this module with the standalone java server, or use
-it to directly start the webdriver binaries for you. Note that the
-latter option does _not_ require the JRE/JDK to be installed, nor does
-it require the selenium standalone server (despite the name of the
-main module!).
+You can use this module to directly start the webdriver servers, after
+[downloading the appropriate ones][dl] and putting the servers in your
+`$PATH`. This method does not require the JRE/JDK to be installed, nor
+does it require the standalone server jar, despite the name of the
+module. In this case, you'll want to use the appropriate class for
+driver construction: either [Selenium::Chrome][],
+[Selenium::Firefox][], [Selenium::PhantomJS][], or
+[Selenium::InternetExplorer][].
+
+You can also use this module with the `selenium-standalone-server.jar`
+to let it handle browser start up for you, and also manage Remote
+connections where the server jar is not running on the same machine as
+your test script is executing. The main class for this method is
+[Selenium::Remote::Driver][].
+
+Regardless of which method you use to construct your browser object,
+all of the classes use the functions listed in the S::R::Driver POD
+documentation, so interacting with the browser, the page, and its
+elements would be the same.
+
+[Selenium::Firefox]: https://metacpan.org/pod/Selenium::Firefox
+[Selenium::Chrome]: https://metacpan.org/pod/Selenium::Chrome
+[Selenium::PhantomJS]: https://metacpan.org/pod/Selenium::PhantomJS
+[Selenium::InternetExplorer]: https://metacpan.org/pod/Selenium::InternetExplorer
+[Selenium::Remote::Driver]: https://metacpan.org/pod/Selenium::Remote::Driver
+[dl]: #no-standalone-server
+
+### no standalone server
+
+- _Firefox 48 & newer_: install the Firefox browser, download
+  [geckodriver][gd] and [put it in your `$PATH`][fxpath]. If the Firefox browser
+  binary is not in the default place for your OS and we cannot locate
+  it via `which`, you may have to specify the binary location during
+  startup.
+
+- _Firefox 47 & older_: install the Firefox browser in the default
+  place for your OS. If the Firefox browser binary is not in the
+  default place for your OS, you may have to specify the binary
+  location during startup.
+
+- _Chrome_: install the Chrome browser, [download Chromedriver][dcd]
+  and get `chromedriver` in your `$PATH`.
+
+- _PhantomJS_: install the PhantomJS binary and get `phantomjs` in
+  your `$PATH`. The driver for PhantomJS, Ghostdriver, is bundled with
+  PhantomJS.
+
+When the browser(s) are installed and you have the appropriate binary
+in your path, you should be able to do the following:
+
+```perl
+my $firefox = Selenium::Firefox->new;
+$firefox->get('http://www.google.com');
+
+my $chrome = Selenium::Chrome->new;
+$chrome->get('http://www.google.com');
+
+my $ghost = Selenium::PhantomJS->new;
+$ghost->get('http://www.google.com');
+```
+
+Note that you can also pass a `binary` argument to any of the above
+classes to manually specify what binary to start. Note that this
+`binary` refers to the driver server, _not_ the browser executable.
+
+```perl
+my $chrome = Selenium::Chrome->new(binary => '~/Downloads/chromedriver');
+```
+
+See the pod for the different modules for more details.
+
+[dcd]: https://sites.google.com/a/chromium.org/chromedriver/downloads
+[fxpath]: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver#Add_executable_to_system_path
+[gd]: https://github.com/mozilla/geckodriver/releases
 
 ### with a standalone server
 
-Download the standalone server and have it running on port 4444:
+Download the [standalone server][] and have it running on port 4444:
 
     $ java -jar selenium-server-standalone-X.XX.X.jar
 
-Then the following should start up Firefox for you:
+As before, have the browsers themselves installed on your machine, and
+download the appropriate binary server, passing its location to the
+server jar during startup.
+
+[standalone server]: http://selenium-release.storage.googleapis.com/index.html
 
 #### Locally
 
@@ -105,46 +172,10 @@ useful [example snippets][ex].
 [ex]:
 https://github.com/gempesaw/Selenium-Remote-Driver/wiki/Example-Snippets
 
-### no standalone server
-
-- _Firefox_: simply have the browser installed in the normal place
-for your OS.
-
-- _Chrome_: install the Chrome browser, [download Chromedriver][dcd]
-and get `chromedriver` in your `$PATH`:
-
-- _PhantomJS_: install the PhantomJS binary and get `phantomjs` in
-  your `$PATH`
-
-As long as the proper binary is available in your path, you should be
-able to do the following:
-
-```perl
-my $firefox = Selenium::Firefox->new;
-$firefox->get('http://www.google.com');
-
-my $chrome = Selenium::Chrome->new;
-$chrome->get('http://www.google.com');
-
-my $ghost = Selenium::PhantomJS->new;
-$ghost->get('http://www.google.com');
-```
-
-Note that you can also pass a `binary` argument to any of the above
-classes to manually specify what binary to start:
-
-```perl
-my $chrome = Selenium::Chrome->new(binary => '~/Downloads/chromedriver');
-```
-
-See the pod for the different modules for more details.
-
-[dcd]: https://sites.google.com/a/chromium.org/chromedriver/downloads
-
 ## Selenium IDE Plugin
 
-[ide-plugin.js](./ide-plugin.js) is a Selenium IDE Plugin which allows you to export tests recorded 
-in Selenium IDE to a perl script.
+[ide-plugin.js](./ide-plugin.js) is a Selenium IDE Plugin which allows
+you to export tests recorded in Selenium IDE to a perl script.
 
 ### Installation in Selenium IDE
 

+ 75 - 29
lib/Selenium/CanStartBinary.pm

@@ -107,12 +107,35 @@ C<127.0.0.1:4444>.
 
 =cut
 
+has '_real_binary' => (
+    is => 'lazy',
+    builder => sub {
+        my ($self) = @_;
+
+        if ($self->_is_old_ff) {
+            return $self->firefox_binary;
+        }
+        else {
+            return $self->binary;
+        }
+    }
+);
+
+has '_is_old_ff' => (
+    is => 'lazy',
+    builder => sub {
+        my ($self) = @_;
+
+        return $self->isa('Selenium::Firefox') && !$self->marionette_enabled;
+    }
+);
+
 has '+port' => (
     is => 'lazy',
     builder => sub {
         my ($self) = @_;
 
-        if ($self->binary) {
+        if ($self->_real_binary) {
             return find_open_port_above($self->binary_port);
         }
         else {
@@ -147,11 +170,11 @@ has 'marionette_port' => (
     builder => sub {
         my ($self) = @_;
 
-        if ($self->isa('Selenium::Firefox') && $self->marionette_enabled) {
-            return find_open_port_above($self->marionette_binary_port);
+        if ($self->_is_old_ff) {
+            return 0;
         }
         else {
-            return;
+            return find_open_port_above($self->marionette_binary_port);
         }
     }
 );
@@ -225,7 +248,7 @@ has 'window_title' => (
     init_arg => undef,
     builder => sub {
         my ($self) = @_;
-        my (undef, undef, $file) = File::Spec->splitpath( $self->binary );
+        my (undef, undef, $file) = File::Spec->splitpath( $self->_real_binary );
         my $port = $self->port;
 
         return $file . ':' . $port;
@@ -270,25 +293,13 @@ sub _build_binary_mode {
     my ($self) = @_;
 
     # We don't know what to do without a binary driver to start up
-    return unless $self->binary;
+    return unless $self->_real_binary;
 
     # Either the user asked for 4444, or we couldn't find an open port
     my $port = $self->port + 0;
     return if $port == 4444;
 
-    if ($self->isa('Selenium::Firefox')) {
-        my $marionette_port = $self->marionette_enabled ?
-        $self->marionette_port : 0;
-
-        my @args = ($port, $marionette_port);
-
-        if ($self->has_firefox_profile) {
-            push @args, $self->firefox_profile;
-            $self->clear_firefox_profile;
-        }
-
-        setup_firefox_binary_env(@args);
-    }
+    $self->_handle_firefox_setup($port);
 
     my $command = $self->_construct_command;
     system($command);
@@ -298,7 +309,37 @@ sub _build_binary_mode {
         return 1;
     }
     else {
-        die 'Unable to connect to the ' . $self->binary . ' binary on port ' . $port;
+        die 'Unable to connect to the ' . $self->_real_binary . ' binary on port ' . $port;
+    }
+}
+
+sub _handle_firefox_setup {
+    my ($self, $port) = @_;
+
+    # This is a no-op for other browsers
+    return unless $self->isa('Selenium::Firefox');
+
+    my $user_profile = $self->has_firefox_profile
+      ? $self->firefox_profile
+      : 0;
+
+    my $profile = setup_firefox_binary_env(
+        $port,
+        $self->marionette_port,
+        $user_profile
+    );
+
+    if ($self->_is_old_ff) {
+        # For non-geckodriver/non-marionette, we want to get rid of
+        # the profile so that we don't accidentally zip it and encode
+        # it down the line while Firefox is trying to read from it.
+        $self->clear_firefox_profile if $self->has_firefox_profile;
+    }
+    else {
+        # For geckodriver/marionette, we keep the enhanced profile around because
+        # we need to send it to geckodriver as a zipped b64-encoded
+        # directory.
+        $self->firefox_profile($profile);
     }
 }
 
@@ -324,7 +365,7 @@ sub shutdown_windows_binary {
     my ($self) = @_;
 
     if (IS_WIN) {
-        if ($self->isa('Selenium::Firefox')) {
+        if ($self->_is_old_ff) {
             # FIXME: Blech, handle a race condition that kills the
             # driver before it's finished cleaning up its sessions. In
             # particular, when the perl process ends, it wants to
@@ -351,11 +392,11 @@ sub DEMOLISH {
     # if we're in global destruction, all bets are off.
     return if $in_gd;
     $self->shutdown_binary;
-};
+}
 
 sub _construct_command {
     my ($self) = @_;
-    my $executable = $self->binary;
+    my $executable = $self->_real_binary;
 
     # Executable path names may have spaces
     $executable = '"' . $executable . '"';
@@ -377,13 +418,18 @@ sub _cmd_prefix {
     if (IS_WIN) {
         my $prefix = 'start "' . $self->window_title . '"';
 
-        # Let's minimize the command windows for the drivers that have
-        # separate binaries - but let's not minimize the Firefox
-        # window itself.
-        if (! $self->isa('Selenium::Firefox')) {
-            $prefix .= ' /MIN ';
+        if ($self->_is_old_ff) {
+            # For older versions of Firefox that run without
+            # marionette, the command we're running actually starts up
+            # the browser itself, so we don't want to minimize it.
+            return $prefix;
+        }
+        else {
+            # If we're firefox with marionette, or any other browser,
+            # the command we're running is the driver, and we don't
+            # need want the command window in the foreground.
+            return $prefix . ' /MIN ';
         }
-        return $prefix;
     }
     else {
         return '';

+ 1 - 2
lib/Selenium/CanStartBinary/FindBinary.pm

@@ -1,7 +1,6 @@
 package Selenium::CanStartBinary::FindBinary;
 
 # ABSTRACT: Coercions for finding webdriver binaries on your system
-use File::Which qw/which/;
 use Cwd qw/abs_path/;
 use File::Which qw/which/;
 use IO::Socket::INET;
@@ -64,7 +63,7 @@ sub _naive_find_binary {
         return $naive_binary;
     }
     else {
-        warn qq(Unable to find the $executable binary in your \$PATH. We'll try falling back to standard Remote Driver);
+        warn qq(Unable to find the $executable binary in your \$PATH.);
         return;
     }
 }

+ 19 - 0
lib/Selenium/Chrome.pm

@@ -8,6 +8,8 @@ extends 'Selenium::Remote::Driver';
 =head1 SYNOPSIS
 
     my $driver = Selenium::Chrome->new;
+    # when you're done
+    $driver->shutdown_binary;
 
 =head1 DESCRIPTION
 
@@ -99,6 +101,23 @@ up to 20 seconds:
 
 See L<Selenium::CanStartBinary/startup_timeout> for more information.
 
+=method shutdown_binary
+
+Call this method instead of L<Selenium::Remote::Driver/quit> to ensure
+that the binary executable is also closed, instead of simply closing
+the browser itself. If the browser is still around, it will call
+C<quit> for you. After that, it will try to shutdown the browser
+binary by making a GET to /shutdown and on Windows, it will attempt to
+do a C<taskkill> on the binary CMD window.
+
+    $self->shutdown_binary;
+
+It doesn't take any arguments, and it doesn't return anything.
+
+We do our best to call this when the C<$driver> option goes out of
+scope, but if that happens during global destruction, there's nothing
+we can do.
+
 =cut
 
 1;

+ 125 - 41
lib/Selenium/Firefox.pm

@@ -2,32 +2,43 @@ package Selenium::Firefox;
 
 # ABSTRACT: Use FirefoxDriver without a Selenium server
 use Moo;
-use Selenium::CanStartBinary::FindBinary qw/coerce_firefox_binary/;
+use Selenium::Firefox::Binary qw/firefox_path/;
+use Selenium::CanStartBinary::FindBinary qw/coerce_simple_binary coerce_firefox_binary/;
 extends 'Selenium::Remote::Driver';
 
 =head1 SYNOPSIS
 
+    # these two are the same, and will only work with Firefox 48 and
+    # greater
     my $driver = Selenium::Firefox->new;
     my $driver = Selenium::Firefox->new( marionette_enabled => 1 );
+    # execute your test as usual
+    $driver->shutdown_binary;
+
+    # For Firefox 47 and older, disable marionette:
+    my $driver = Selenium::Firefox->new( marionette_enabled => 0 );
+    $driver->shutdown_binary;
 
 =head1 DESCRIPTION
 
 This class allows you to use the FirefoxDriver without needing the JRE
-or a selenium server running. When you refrain from passing the
-C<remote_server_addr> and C<port> arguments, we will search for the
-Firefox executable in your $PATH. We'll try to start the binary
-connect to it, shutting it down at the end of the test.
+or a selenium server running. Unlike starting up an instance of
+S::R::D, do not pass the C<remote_server_addr> and C<port> arguments,
+and we will search for the Firefox executable in your $PATH. We'll try
+to start the binary, connect to it, and shut it down at the end of the
+test.
 
 If the Firefox application is not found in the expected places, we'll
 fall back to the default L<Selenium::Remote::Driver> behavior of
 assuming defaults of 127.0.0.1:4444 after waiting a few seconds.
 
-If you specify a remote server address, or a port, we'll assume you
-know what you're doing and take no additional behavior.
+If you specify a remote server address, or a port, our assumption is
+that you are doing standard S::R::D behavior and we will not attempt
+any binary startup.
 
 If you're curious whether your Selenium::Firefox instance is using a
 separate Firefox binary, or through the selenium server, you can check
-the C<binary_mode> attr after instantiation.
+the value of the C<binary_mode> attr after instantiation.
 
 =cut
 
@@ -38,17 +49,23 @@ has '+browser_name' => (
 
 =attr binary
 
-Optional: specify the path to your binary. If you don't specify
-anything, we'll try to find it on our own in the default installation
-paths for Firefox. If your Firefox is elsewhere, we probably won't be
-able to find it, so you may be well served by specifying it yourself.
+Optional: specify the path to the C<geckodriver> binary - this is NOT
+the path to the Firefox browser. To specify the path to your Firefox
+browser binary, see the L</firefox_binary> attr.
+
+For Firefox 48 and greater, this is the path to your C<geckodriver>
+executable. If you don't specify anything, we'll search for
+C<geckodriver> in your $PATH.
+
+For Firefox 47 and older, this attribute does not apply, because the
+older FF browsers do not use the separate driver binary startup.
 
 =cut
 
 has 'binary' => (
     is => 'lazy',
-    coerce => \&coerce_firefox_binary,
-    default => sub { 'firefox' },
+    coerce => \&coerce_simple_binary,
+    default => sub { 'geckodriver' },
     predicate => 1
 );
 
@@ -71,22 +88,54 @@ has 'binary_port' => (
     default => sub { 9090 }
 );
 
+=attr firefox_profile
+
+Optional: Pass in an instance of L<Selenium::Firefox::Profile>
+pre-configured as you please. The preferences you specify will be
+merged with the ones necessary for setting up webdriver, and as a
+result some options may be overwritten or ignored.
+
+    my $profile = Selenium::Firefox::Profile->new;
+    my $firefox = Selenium::Firefox->new(
+        firefox_profile => $profile
+    );
+
+=cut
+
 has '_binary_args' => (
     is => 'lazy',
     builder => sub {
         my ($self) = @_;
 
-        my $args = ' -no-remote';
-        if( $self->marionette_enabled ) {
-            $args .= ' -marionette';
+        if ( $self->marionette_enabled ) {
+            my $args = ' --port ' . $self->port;
+            $args .= ' --marionette-port ' . $self->marionette_binary_port;
+
+            if ( $self->has_firefox_binary ) {
+                $args .= ' --binary "' . $self->firefox_binary . '"';
+            }
+
+            return $args;
+        }
+        else {
+            return ' -no-remote';
         }
-        return $args;
     }
 );
 
 has '+wd_context_prefix' => (
     is => 'ro',
-    default => sub { '/hub' }
+    default => sub {
+        my ($self) = @_;
+
+        if ($self->marionette_enabled) {
+            return '';
+        }
+        else {
+            return '/hub';
+        }
+
+    }
 );
 
 =attr marionette_binary_port
@@ -97,15 +146,15 @@ there's no a priori guarantee that this will be an open port, this is
 _not_ necessarily the port that we end up using - if the port here is
 already bound, we'll search above it until we find an open one.
 
-See L<Selenium::CanStartBinary/port> for more details, and
-L<Selenium::Remote::Driver/port> after instantiation to see what the
-actual port turned out to be.
-
     Selenium::Firefox->new(
         marionette_enabled     => 1,
         marionette_binary_port => 12345,
     );
 
+Attempting to specify a C<marionette_binary_port> in conjunction with
+setting C<marionette_enabled> does not make sense and will most likely
+not do anything useful.
+
 =cut
 
 has 'marionette_binary_port' => (
@@ -115,34 +164,46 @@ has 'marionette_binary_port' => (
 
 =attr marionette_enabled
 
-Optional: specify whether L<marionette|https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette>
-should be enabled or not. If you enable the marionette_enabled flag,
-Firefox is launched with marionette server listening to
-C<marionette_binary_port>.
+Optional: specify whether
+L<marionette|https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette>
+should be enabled or not. By default, marionette is enabled, which
+assumes you are running with Firefox 48 or newer. To use this module
+to start Firefox 47 or older, you must pass C<marionette_enabled =>
+0>.
+
+    my $ff48 = Selenium::Firefox->new( marionette_enabled => 1 ); # defaults to 1
+    my $ff47 = Selenium::Firefox->new( marionette_enabled => 0 );
+
+=cut
 
-The firefox binary must have been built with this funtionality and it's
-available in L<all recent Firefox binaries|https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Builds>.
+has 'marionette_enabled' => (
+    is => 'lazy',
+    default => 1
+);
 
-Note: L<Selenium::Remote::Driver> does not yet provide a marionette
-client. It's up to the user to use a client or a marionette-to-webdriver
-proxy to communicate with the marionette server.
+=attr firefox_binary
 
-    Selenium::Firefox->new( marionette_enabled => 1 );
+Optional: specify the path to the Firefox browser executable. Although
+we will attempt to locate this in your $PATH, you may specify it
+explicitly here. Note that path here must point to a file that exists
+and is executable, or we will croak.
 
-and Firefox will have 2 ports open. One for webdriver and one
-for marionette:
+For Firefox 48 and newer, this will be passed to C<geckodriver> such
+that it will attempt to start up the Firefox at the specified path.
 
-    netstat -tlp | grep firefox
-    tcp    0    0    localhost:9090    *:*    LISTEN    23456/firefox
-    tcp    0    0    localhost:2828    *:*    LISTEN    23456/firefox
+For Firefox 47 and older, this browser path will be the file that we
+directly start up.
 
 =cut
 
-has 'marionette_enabled' => (
-    is  => 'lazy',
-    default => 0
+has 'firefox_binary' => (
+    is => 'ro',
+    coerce => \&coerce_firefox_binary,
+    predicate => 1,
+    builder => 'firefox_path'
 );
 
+
 with 'Selenium::CanStartBinary';
 
 =attr custom_args
@@ -151,6 +212,12 @@ Optional: specify any additional command line arguments you'd like
 invoked during the binary startup. See
 L<Selenium::CanStartBinary/custom_args> for more information.
 
+For Firefox 48 and newer, these arguments will be passed to
+geckodriver during start up.
+
+For Firefox 47 and older, these arguments will be passed to the
+Firefox browser during start up.
+
 =attr startup_timeout
 
 Optional: specify how long to wait for the binary to start itself and
@@ -162,6 +229,23 @@ up to 20 seconds:
 
 See L<Selenium::CanStartBinary/startup_timeout> for more information.
 
+=method shutdown_binary
+
+Call this method instead of L<Selenium::Remote::Driver/quit> to ensure
+that the binary executable is also closed, instead of simply closing
+the browser itself. If the browser is still around, it will call
+C<quit> for you. After that, it will try to shutdown the browser
+binary by making a GET to /shutdown and on Windows, it will attempt to
+do a C<taskkill> on the binary CMD window.
+
+    $self->shutdown_binary;
+
+It doesn't take any arguments, and it doesn't return anything.
+
+We do our best to call this when the C<$driver> option goes out of
+scope, but if that happens during global destruction, there's nothing
+we can do.
+
 =cut
 
 1;

+ 12 - 5
lib/Selenium/Firefox/Binary.pm

@@ -67,13 +67,20 @@ sub setup_firefox_binary_env {
     my ($port, $marionette_port, $caller_profile) = @_;
 
     $profile = $caller_profile || Selenium::Firefox::Profile->new;
-    $profile->add_webdriver($port);
+    $profile->add_webdriver($port, $marionette_port);
     $profile->add_marionette($marionette_port);
 
-    $ENV{'XRE_PROFILE_PATH'} = $profile->_layout_on_disk;
-    $ENV{'MOZ_NO_REMOTE'} = '1';             # able to launch multiple instances
-    $ENV{'MOZ_CRASHREPORTER_DISABLE'} = '1'; # disable breakpad
-    $ENV{'NO_EM_RESTART'} = '1';             # prevent the binary from detaching from the console.log
+    # For non-geckodriver/marionette startup, we instruct Firefox to
+    # use the profile by specifying the appropriate environment
+    # variables for it to hook onto.
+    if (! $marionette_port) {
+        $ENV{'XRE_PROFILE_PATH'} = $profile->_layout_on_disk;
+        $ENV{'MOZ_NO_REMOTE'} = '1'; # able to launch multiple instances
+        $ENV{'MOZ_CRASHREPORTER_DISABLE'} = '1'; # disable breakpad
+        $ENV{'NO_EM_RESTART'} = '1'; # prevent the binary from detaching from the console.log
+    }
+
+    return $profile;
 }
 
 

+ 51 - 13
lib/Selenium/Firefox/Profile.pm

@@ -23,7 +23,7 @@ use XML::Simple;
 You can use this module to create a custom Firefox Profile for your
 Selenium tests. Currently, you can set browser preferences and add
 extensions to the profile before passing it in the constructor for a
-new Selenium::Remote::Driver.
+new L</Selenium::Remote::Driver> or L</Selenium::Firefox>.
 
 =head1 SYNPOSIS
 
@@ -185,16 +185,42 @@ sub add_extension {
 
 =method add_webdriver
 
-Primarily for internal use, we add the webdriver extension to the
-current Firefox profile.
+Primarily for internal use, we set the appropriate firefox preferences
+for a new geckodriver session.
 
 =cut
 
 sub add_webdriver {
-    my ($self, $port) = @_;
+    my ($self, $port, $is_marionette) = @_;
+
+    my $prefs = $self->_load_prefs;
+    my $current_user_prefs = $self->{user_prefs};
+
+    $self->set_preference(
+        %{ $prefs->{mutable} },
+        # having the user prefs here allows them to overwrite the
+        # mutable loaded prefs
+        %{ $current_user_prefs },
+        # but the frozen ones cannot be overwritten
+        %{ $prefs->{frozen} },
+        'webdriver_firefox_port' => $port
+    );
+
+    if (! $is_marionette) {
+        $self->_add_webdriver_xpi;
+    }
+
+    return $self;
+}
+
+sub _load_prefs {
+    # The appropriate webdriver preferences are stored in an adjacent
+    # JSON file; it's useful things like disabling default browser
+    # checks and setting an empty single page as the start up tab
+    # configuration. Unfortunately, these change with each version of
+    # webdriver.
 
     my $this_dir = dirname(abs_path(__FILE__));
-    my $webdriver_extension = $this_dir . '/webdriver.xpi';
     my $default_prefs_filename = $this_dir . '/webdriver_prefs.json';
 
     my $json;
@@ -204,17 +230,29 @@ sub add_webdriver {
         $json = <$fh>;
         close ($fh);
     }
-    my $webdriver_prefs = decode_json($json);
-    my $current_user_prefs = $self->{user_prefs};
 
-    $self->set_preference(
-        %{ $webdriver_prefs->{mutable} },
-        %{ $current_user_prefs }
-    );
-    $self->set_preference(%{ $webdriver_prefs->{frozen} });
+    my $prefs = decode_json($json);
+
+    return $prefs;
+}
+
+=method add_webdriver_xpi
+
+Primarily for internal use. This adds the fxgoogle .xpi that is used
+for webdriver communication in FF47 and older. For FF48 and newer, the
+old method using an extension to orchestrate the webdriver
+communication with the Firefox browser has been obsoleted by the
+introduction of C<geckodriver>.
+
+=cut
+
+sub _add_webdriver_xpi {
+    my ($self) = @_;
+
+    my $this_dir = dirname(abs_path(__FILE__));
+    my $webdriver_extension = $this_dir . '/webdriver.xpi';
 
     $self->add_extension($webdriver_extension);
-    $self->set_preference('webdriver_firefox_port', $port);
 }
 
 =method add_marionette

+ 21 - 0
lib/Selenium/InternetExplorer.pm

@@ -7,6 +7,8 @@ extends 'Selenium::Remote::Driver';
 =head1 SYNOPSIS
 
     my $driver = Selenium::InternetExplorer->new;
+    # when you're done
+    $driver->shutdown_binary;
 
 =cut
 
@@ -20,4 +22,23 @@ has '+platform' => (
     default => sub { 'WINDOWS' }
 );
 
+=method shutdown_binary
+
+Call this method instead of L<Selenium::Remote::Driver/quit> to ensure
+that the binary executable is also closed, instead of simply closing
+the browser itself. If the browser is still around, it will call
+C<quit> for you. After that, it will try to shutdown the browser
+binary by making a GET to /shutdown and on Windows, it will attempt to
+do a C<taskkill> on the binary CMD window.
+
+    $self->shutdown_binary;
+
+It doesn't take any arguments, and it doesn't return anything.
+
+We do our best to call this when the C<$driver> option goes out of
+scope, but if that happens during global destruction, there's nothing
+we can do.
+
+=cut
+
 1;

+ 19 - 0
lib/Selenium/PhantomJS.pm

@@ -8,6 +8,8 @@ extends 'Selenium::Remote::Driver';
 =head1 SYNOPSIS
 
     my $driver = Selenium::PhantomJS->new;
+    # when you're done
+    $driver->shutdown_binary;
 
 =head1 DESCRIPTION
 
@@ -109,6 +111,23 @@ up to 20 seconds:
 
 See L<Selenium::CanStartBinary/startup_timeout> for more information.
 
+=method shutdown_binary
+
+Call this method instead of L<Selenium::Remote::Driver/quit> to ensure
+that the binary executable is also closed, instead of simply closing
+the browser itself. If the browser is still around, it will call
+C<quit> for you. After that, it will try to shutdown the browser
+binary by making a GET to /shutdown and on Windows, it will attempt to
+do a C<taskkill> on the binary CMD window.
+
+    $self->shutdown_binary;
+
+It doesn't take any arguments, and it doesn't return anything.
+
+We do our best to call this when the C<$driver> option goes out of
+scope, but if that happens during global destruction, there's nothing
+we can do.
+
 =cut
 
 1;

+ 7 - 2
lib/Selenium/Remote/Driver.pm

@@ -657,7 +657,7 @@ sub new_session {
     }
 
     if ($args->{desiredCapabilities}->{browserName} =~ /firefox/i
-          && $self->has_firefox_profile) {
+        && $self->has_firefox_profile) {
         $args->{desiredCapabilities}->{firefox_profile} = $self->firefox_profile->_encode;
     }
 
@@ -674,7 +674,12 @@ sub new_desired_session {
 
 sub _request_new_session {
     my ( $self, $args ) = @_;
-    $self->remote_conn->check_status();
+
+    # geckodriver has not yet implemented the GET /status endpoint
+    # https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver/status
+    if (! $self->isa('Selenium::Firefox')) {
+        $self->remote_conn->check_status();
+    }
     # command => 'newSession' to fool the tests of commands implemented
     # TODO: rewrite the testing better, this is so fragile.
     my $resource_new_session = {

+ 65 - 37
t/CanStartBinary.t

@@ -68,56 +68,84 @@ CHROME: {
 
 FIREFOX: {
   SKIP: {
-        skip 'Firefox will not start up on UNIX without a display', 3
+        skip 'Firefox will not start up on UNIX without a display', 6
           if ($^O ne 'MSWin32' && ! $ENV{DISPLAY});
-        my $binary = Selenium::Firefox::Binary::firefox_path();
-        skip 'Firefox binary not found in path', 3
-          unless $binary;
-
-        ok(-x $binary, 'we can find some sort of firefox');
-
-        my $firefox = Selenium::Firefox->new;
-        isnt( $firefox->port, 4444, 'firefox can start up its own binary');
-        ok( Selenium::CanStartBinary::probe_port( $firefox->port ), 'the firefox binary is listening on its port');
-        $firefox->shutdown_binary;
-
-      PROFILE: {
-            my $encoded = 0;
-            {
-                package FFProfile;
-                use Moo;
-                extends 'Selenium::Firefox::Profile';
-
-                sub _encode { $encoded++ };
-                1;
-            }
 
-            my $p = FFProfile->new;
-            my $firefox_with_profile = Selenium::Firefox->new(firefox_profile => $p);
-            $firefox_with_profile->shutdown_binary;
-            is($encoded, 0, 'Binary firefox does not encode profile unnecessarily');
+      NEWER: {
+            my $has_geckodriver = which('geckodriver');
+            skip 'Firefox geckodriver not found in path', 3
+              unless $has_geckodriver;
+
+            my $firefox = Selenium::Firefox->new;
+            isnt( $firefox->port, 4444, 'firefox can start up its own binary');
+            ok(Selenium::CanStartBinary::probe_port($firefox->port),
+               'the firefox binary is listening on its port');
+
+            ok(Selenium::CanStartBinary::probe_port($firefox->marionette_port),
+               'the firefox binary is listening on its marionette port');
+            $firefox->shutdown_binary;
         }
 
-        my $firefox_marionette = Selenium::Firefox->new(
-            marionette_enabled => 1
-        );
-        isnt( $firefox->port, 4444, 'firefox can start up its own binary');
-        ok( Selenium::CanStartBinary::probe_port( $firefox_marionette->marionette_port ), 'the firefox binary with marionette enabled is listening on its marionette port');
+      OLDER: {
+            # These are admittedly a very brittle test, so it's getting
+            # skipped almost all the time.
+            my $ff47_binary = '/Applications/Firefox47.app/Contents/MacOS/firefox-bin';
+            skip 'Firefox 47 compatibility tests require FF47 to be installed', 3
+              unless -x $ff47_binary;
+
+            my $ff47 = Selenium::Firefox->new(
+                marionette_enabled => 0,
+                firefox_binary => $ff47_binary
+            );
+            isnt( $ff47->port, 4444, 'older Firefox47 can start up its own binary');
+            ok( Selenium::CanStartBinary::probe_port( $ff47->port ),
+                'the older Firefox47 is listening on its port');
+            $ff47->shutdown_binary;
+
+
+          PROFILE: {
+                my $encoded = 0;
+                {
+                    package FFProfile;
+                    use Moo;
+                    extends 'Selenium::Firefox::Profile';
+
+                    sub _encode { $encoded++ };
+                    1;
+                }
+
+                my $p = FFProfile->new;
+
+                # we don't need to keep this browser object around at all,
+                # we just want to run through the construction and confirm
+                # that nothing gets encoded
+                Selenium::Firefox->new(
+                    marionette_enabled => 0,
+                    firefox_binary => $ff47_binary,
+                    firefox_profile => $p
+                )->shutdown_binary;
+                is($encoded, 0, 'older Firefox47 does not encode profile unnecessarily');
+            }
+
+        }
     }
 }
 
 TIMEOUT: {
+    my $has_geckodriver = which('geckodriver');
+    skip 'Firefox geckodriver not found in path', 1
+      unless $has_geckodriver;
+
     my $binary = Selenium::Firefox::Binary::firefox_path();
-    skip 'Firefox binary not found in path', 3
+    skip 'Firefox browser not found in path', 1
       unless $binary;
 
-    # Force the port check to exhaust the wait_until timeout so that
-    # we can exercise the startup_timeout constructor option
-    # functionality.
+    # Override the binary command construction so that no web driver
+    # will start up.
     Sub::Install::reinstall_sub({
-        code => sub { return 0 },
+        code => sub { return '' },
         into => 'Selenium::CanStartBinary',
-        as => 'probe_port'
+        as => '_construct_command'
     });
 
     my $start = time;