Browse Source

Merge pull request #271 from gempesaw/geckodriver

Add compatibility with marionette (geckodriver) for firefox 48

- Fix #260
Daniel Gempesaw 9 years ago
parent
commit
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
 [Selenium WebDriver][wd] is a test tool that allows you to write
 automated web application UI tests in any programming language against
 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
 module is a Perl implementation of the client for the Webdriver
 [JSONWireProtocol that Selenium provides.][jsonwire]
 [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
 [jsonwire]: https://code.google.com/p/selenium/wiki/JsonWireProtocol
 [standalone]: http://selenium-release.storage.googleapis.com/index.html
 [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
 $ 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
 [installation docs]: /INSTALL.md
 
 
 ## Usage
 ## 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
 ### 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
     $ 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
 #### Locally
 
 
@@ -105,46 +172,10 @@ useful [example snippets][ex].
 [ex]:
 [ex]:
 https://github.com/gempesaw/Selenium-Remote-Driver/wiki/Example-Snippets
 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
 ## 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
 ### Installation in Selenium IDE
 
 

+ 75 - 29
lib/Selenium/CanStartBinary.pm

@@ -107,12 +107,35 @@ C<127.0.0.1:4444>.
 
 
 =cut
 =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' => (
 has '+port' => (
     is => 'lazy',
     is => 'lazy',
     builder => sub {
     builder => sub {
         my ($self) = @_;
         my ($self) = @_;
 
 
-        if ($self->binary) {
+        if ($self->_real_binary) {
             return find_open_port_above($self->binary_port);
             return find_open_port_above($self->binary_port);
         }
         }
         else {
         else {
@@ -147,11 +170,11 @@ has 'marionette_port' => (
     builder => sub {
     builder => sub {
         my ($self) = @_;
         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 {
         else {
-            return;
+            return find_open_port_above($self->marionette_binary_port);
         }
         }
     }
     }
 );
 );
@@ -225,7 +248,7 @@ has 'window_title' => (
     init_arg => undef,
     init_arg => undef,
     builder => sub {
     builder => sub {
         my ($self) = @_;
         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;
         my $port = $self->port;
 
 
         return $file . ':' . $port;
         return $file . ':' . $port;
@@ -270,25 +293,13 @@ sub _build_binary_mode {
     my ($self) = @_;
     my ($self) = @_;
 
 
     # We don't know what to do without a binary driver to start up
     # 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
     # Either the user asked for 4444, or we couldn't find an open port
     my $port = $self->port + 0;
     my $port = $self->port + 0;
     return if $port == 4444;
     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;
     my $command = $self->_construct_command;
     system($command);
     system($command);
@@ -298,7 +309,37 @@ sub _build_binary_mode {
         return 1;
         return 1;
     }
     }
     else {
     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) = @_;
     my ($self) = @_;
 
 
     if (IS_WIN) {
     if (IS_WIN) {
-        if ($self->isa('Selenium::Firefox')) {
+        if ($self->_is_old_ff) {
             # FIXME: Blech, handle a race condition that kills the
             # FIXME: Blech, handle a race condition that kills the
             # driver before it's finished cleaning up its sessions. In
             # driver before it's finished cleaning up its sessions. In
             # particular, when the perl process ends, it wants to
             # 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.
     # if we're in global destruction, all bets are off.
     return if $in_gd;
     return if $in_gd;
     $self->shutdown_binary;
     $self->shutdown_binary;
-};
+}
 
 
 sub _construct_command {
 sub _construct_command {
     my ($self) = @_;
     my ($self) = @_;
-    my $executable = $self->binary;
+    my $executable = $self->_real_binary;
 
 
     # Executable path names may have spaces
     # Executable path names may have spaces
     $executable = '"' . $executable . '"';
     $executable = '"' . $executable . '"';
@@ -377,13 +418,18 @@ sub _cmd_prefix {
     if (IS_WIN) {
     if (IS_WIN) {
         my $prefix = 'start "' . $self->window_title . '"';
         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 {
     else {
         return '';
         return '';

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

@@ -1,7 +1,6 @@
 package Selenium::CanStartBinary::FindBinary;
 package Selenium::CanStartBinary::FindBinary;
 
 
 # ABSTRACT: Coercions for finding webdriver binaries on your system
 # ABSTRACT: Coercions for finding webdriver binaries on your system
-use File::Which qw/which/;
 use Cwd qw/abs_path/;
 use Cwd qw/abs_path/;
 use File::Which qw/which/;
 use File::Which qw/which/;
 use IO::Socket::INET;
 use IO::Socket::INET;
@@ -64,7 +63,7 @@ sub _naive_find_binary {
         return $naive_binary;
         return $naive_binary;
     }
     }
     else {
     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;
         return;
     }
     }
 }
 }

+ 19 - 0
lib/Selenium/Chrome.pm

@@ -8,6 +8,8 @@ extends 'Selenium::Remote::Driver';
 =head1 SYNOPSIS
 =head1 SYNOPSIS
 
 
     my $driver = Selenium::Chrome->new;
     my $driver = Selenium::Chrome->new;
+    # when you're done
+    $driver->shutdown_binary;
 
 
 =head1 DESCRIPTION
 =head1 DESCRIPTION
 
 
@@ -99,6 +101,23 @@ up to 20 seconds:
 
 
 See L<Selenium::CanStartBinary/startup_timeout> for more information.
 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
 =cut
 
 
 1;
 1;

+ 125 - 41
lib/Selenium/Firefox.pm

@@ -2,32 +2,43 @@ package Selenium::Firefox;
 
 
 # ABSTRACT: Use FirefoxDriver without a Selenium server
 # ABSTRACT: Use FirefoxDriver without a Selenium server
 use Moo;
 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';
 extends 'Selenium::Remote::Driver';
 
 
 =head1 SYNOPSIS
 =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;
     my $driver = Selenium::Firefox->new( marionette_enabled => 1 );
     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
 =head1 DESCRIPTION
 
 
 This class allows you to use the FirefoxDriver without needing the JRE
 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
 If the Firefox application is not found in the expected places, we'll
 fall back to the default L<Selenium::Remote::Driver> behavior of
 fall back to the default L<Selenium::Remote::Driver> behavior of
 assuming defaults of 127.0.0.1:4444 after waiting a few seconds.
 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
 If you're curious whether your Selenium::Firefox instance is using a
 separate Firefox binary, or through the selenium server, you can check
 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
 =cut
 
 
@@ -38,17 +49,23 @@ has '+browser_name' => (
 
 
 =attr binary
 =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
 =cut
 
 
 has 'binary' => (
 has 'binary' => (
     is => 'lazy',
     is => 'lazy',
-    coerce => \&coerce_firefox_binary,
-    default => sub { 'firefox' },
+    coerce => \&coerce_simple_binary,
+    default => sub { 'geckodriver' },
     predicate => 1
     predicate => 1
 );
 );
 
 
@@ -71,22 +88,54 @@ has 'binary_port' => (
     default => sub { 9090 }
     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' => (
 has '_binary_args' => (
     is => 'lazy',
     is => 'lazy',
     builder => sub {
     builder => sub {
         my ($self) = @_;
         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' => (
 has '+wd_context_prefix' => (
     is => 'ro',
     is => 'ro',
-    default => sub { '/hub' }
+    default => sub {
+        my ($self) = @_;
+
+        if ($self->marionette_enabled) {
+            return '';
+        }
+        else {
+            return '/hub';
+        }
+
+    }
 );
 );
 
 
 =attr marionette_binary_port
 =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
 _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.
 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(
     Selenium::Firefox->new(
         marionette_enabled     => 1,
         marionette_enabled     => 1,
         marionette_binary_port => 12345,
         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
 =cut
 
 
 has 'marionette_binary_port' => (
 has 'marionette_binary_port' => (
@@ -115,34 +164,46 @@ has 'marionette_binary_port' => (
 
 
 =attr marionette_enabled
 =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
 =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';
 with 'Selenium::CanStartBinary';
 
 
 =attr custom_args
 =attr custom_args
@@ -151,6 +212,12 @@ Optional: specify any additional command line arguments you'd like
 invoked during the binary startup. See
 invoked during the binary startup. See
 L<Selenium::CanStartBinary/custom_args> for more information.
 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
 =attr startup_timeout
 
 
 Optional: specify how long to wait for the binary to start itself and
 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.
 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
 =cut
 
 
 1;
 1;

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

@@ -67,13 +67,20 @@ sub setup_firefox_binary_env {
     my ($port, $marionette_port, $caller_profile) = @_;
     my ($port, $marionette_port, $caller_profile) = @_;
 
 
     $profile = $caller_profile || Selenium::Firefox::Profile->new;
     $profile = $caller_profile || Selenium::Firefox::Profile->new;
-    $profile->add_webdriver($port);
+    $profile->add_webdriver($port, $marionette_port);
     $profile->add_marionette($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
 You can use this module to create a custom Firefox Profile for your
 Selenium tests. Currently, you can set browser preferences and add
 Selenium tests. Currently, you can set browser preferences and add
 extensions to the profile before passing it in the constructor for a
 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
 =head1 SYNPOSIS
 
 
@@ -185,16 +185,42 @@ sub add_extension {
 
 
 =method add_webdriver
 =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
 =cut
 
 
 sub add_webdriver {
 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 $this_dir = dirname(abs_path(__FILE__));
-    my $webdriver_extension = $this_dir . '/webdriver.xpi';
     my $default_prefs_filename = $this_dir . '/webdriver_prefs.json';
     my $default_prefs_filename = $this_dir . '/webdriver_prefs.json';
 
 
     my $json;
     my $json;
@@ -204,17 +230,29 @@ sub add_webdriver {
         $json = <$fh>;
         $json = <$fh>;
         close ($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->add_extension($webdriver_extension);
-    $self->set_preference('webdriver_firefox_port', $port);
 }
 }
 
 
 =method add_marionette
 =method add_marionette

+ 21 - 0
lib/Selenium/InternetExplorer.pm

@@ -7,6 +7,8 @@ extends 'Selenium::Remote::Driver';
 =head1 SYNOPSIS
 =head1 SYNOPSIS
 
 
     my $driver = Selenium::InternetExplorer->new;
     my $driver = Selenium::InternetExplorer->new;
+    # when you're done
+    $driver->shutdown_binary;
 
 
 =cut
 =cut
 
 
@@ -20,4 +22,23 @@ has '+platform' => (
     default => sub { 'WINDOWS' }
     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;
 1;

+ 19 - 0
lib/Selenium/PhantomJS.pm

@@ -8,6 +8,8 @@ extends 'Selenium::Remote::Driver';
 =head1 SYNOPSIS
 =head1 SYNOPSIS
 
 
     my $driver = Selenium::PhantomJS->new;
     my $driver = Selenium::PhantomJS->new;
+    # when you're done
+    $driver->shutdown_binary;
 
 
 =head1 DESCRIPTION
 =head1 DESCRIPTION
 
 
@@ -109,6 +111,23 @@ up to 20 seconds:
 
 
 See L<Selenium::CanStartBinary/startup_timeout> for more information.
 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
 =cut
 
 
 1;
 1;

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

@@ -657,7 +657,7 @@ sub new_session {
     }
     }
 
 
     if ($args->{desiredCapabilities}->{browserName} =~ /firefox/i
     if ($args->{desiredCapabilities}->{browserName} =~ /firefox/i
-          && $self->has_firefox_profile) {
+        && $self->has_firefox_profile) {
         $args->{desiredCapabilities}->{firefox_profile} = $self->firefox_profile->_encode;
         $args->{desiredCapabilities}->{firefox_profile} = $self->firefox_profile->_encode;
     }
     }
 
 
@@ -674,7 +674,12 @@ sub new_desired_session {
 
 
 sub _request_new_session {
 sub _request_new_session {
     my ( $self, $args ) = @_;
     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
     # command => 'newSession' to fool the tests of commands implemented
     # TODO: rewrite the testing better, this is so fragile.
     # TODO: rewrite the testing better, this is so fragile.
     my $resource_new_session = {
     my $resource_new_session = {

+ 65 - 37
t/CanStartBinary.t

@@ -68,56 +68,84 @@ CHROME: {
 
 
 FIREFOX: {
 FIREFOX: {
   SKIP: {
   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});
           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: {
 TIMEOUT: {
+    my $has_geckodriver = which('geckodriver');
+    skip 'Firefox geckodriver not found in path', 1
+      unless $has_geckodriver;
+
     my $binary = Selenium::Firefox::Binary::firefox_path();
     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;
       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({
     Sub::Install::reinstall_sub({
-        code => sub { return 0 },
+        code => sub { return '' },
         into => 'Selenium::CanStartBinary',
         into => 'Selenium::CanStartBinary',
-        as => 'probe_port'
+        as => '_construct_command'
     });
     });
 
 
     my $start = time;
     my $start = time;