소스 검색

Merge pull request #189 from gempesaw/fix-189-no-selenium-server

Run locally without selenium server
Daniel Gempesaw 10 년 전
부모
커밋
825dc2ce7e

+ 5 - 2
cpanfile

@@ -1,4 +1,3 @@
-requires "Archive::Extract" => "0";
 requires "Archive::Zip" => "0";
 requires "Carp" => "0";
 requires "Cwd" => "0";
@@ -6,13 +5,17 @@ requires "Data::Dumper" => "0";
 requires "Exporter" => "0";
 requires "File::Basename" => "0";
 requires "File::Copy" => "0";
+requires "File::Spec" => "0";
 requires "File::Spec::Functions" => "0";
 requires "File::Temp" => "0";
+requires "File::Which" => "0";
 requires "HTTP::Headers" => "0";
 requires "HTTP::Request" => "0";
 requires "HTTP::Response" => "0";
 requires "IO::Compress::Zip" => "0";
 requires "IO::Socket" => "0";
+requires "IO::Socket::INET" => "0";
+requires "IO::Uncompress::Unzip" => "0";
 requires "JSON" => "0";
 requires "LWP::UserAgent" => "0";
 requires "List::MoreUtils" => "0";
@@ -25,6 +28,7 @@ requires "Sub::Install" => "0";
 requires "Test::Builder" => "0";
 requires "Test::LongString" => "0";
 requires "Try::Tiny" => "0";
+requires "XML::Simple" => "0";
 requires "base" => "0";
 requires "constant" => "0";
 requires "namespace::clean" => "0";
@@ -35,7 +39,6 @@ requires "warnings" => "0";
 on 'test' => sub {
   requires "File::stat" => "0";
   requires "FindBin" => "0";
-  requires "IO::Socket::INET" => "0";
   requires "LWP::Simple" => "0";
   requires "Test::Exception" => "0";
   requires "Test::Fatal" => "0";

+ 318 - 0
lib/Selenium/CanStartBinary.pm

@@ -0,0 +1,318 @@
+package Selenium::CanStartBinary;
+
+# ABSTRACT: Teach a WebDriver how to start its own binary aka no JRE!
+use File::Spec;
+use Selenium::CanStartBinary::ProbePort qw/find_open_port_above probe_port/;
+use Selenium::Firefox::Binary qw/setup_firefox_binary_env/;
+use Selenium::Waiter qw/wait_until/;
+use Moo::Role;
+
+=head1 SYNOPSIS
+
+    package My::Selenium::Chrome {
+        use Moo;
+        extends 'Selenium::Remote::Driver';
+
+        has 'binary' => ( is => 'ro', default => 'chromedriver' );
+        has 'binary_port' => ( is => 'ro', default => 9515 );
+        with 'Selenium::CanStartBinary';
+        1
+    };
+
+    my $chrome_via_binary = My::Selenium::Chrome->new;
+    my $chrome_with_path  = My::Selenium::Chrome->new(
+        binary => './chromedriver'
+    );
+
+=head1 DESCRIPTION
+
+This role takes care of the details for starting up a Webdriver
+instance. It does not do any downloading or installation of any sort -
+you're still responsible for obtaining and installing the necessary
+binaries into your C<$PATH> for this role to find. You may be
+interested in L<Selenium::Chrome>, L<Selenium::Firefox>, or
+L<Selenium::PhantomJS> if you're looking for classes that already
+consume this role.
+
+The role determines whether or not it should try to do its own magic
+based on whether the consuming class is instantiated with a
+C<remote_server_addr> and/or C<port>.
+
+    # We'll start up the Chrome binary for you
+    my $chrome_via_binary = Selenium::Chrome->new;
+
+    # Look for a selenium server running on 4444.
+    my $chrome_via_server = Selenium::Chrome->new( port => 4444 );
+
+If they're missing, we assume the user wants to use a webdriver
+directly and act accordingly. We handle finding the proper associated
+binary (or you can specify it with L</binary>), figuring out what
+arguments it wants, setting up any necessary environments, and
+starting up the binary.
+
+There's a number of TODOs left over - namely Windows support is
+severely lacking, and we're pretty naive when we attempt to locate the
+executables on our own.
+
+In the following documentation, C<required> refers to when you're
+consuming the role, not the C<required> when you're instantiating a
+class that has already consumed the role.
+
+=attr binary
+
+Required: Specify the path to the executable in question, or the name
+of the executable for us to find via L<File::Which/which>.
+
+=cut
+
+requires 'binary';
+
+=attr binary_port
+
+Required: Specify a default port that for the webdriver binary to try
+to bind to. If that port is unavailable, we'll probe above that port
+until we find a valid one.
+
+=cut
+
+requires 'binary_port';
+
+=attr port
+
+The role will attempt to determine the proper port for us. Consuming
+roles should set a default port in L</binary_port> at which we will
+begin searching for an open port.
+
+Note that if we cannot locate a suitable L</binary>, port will be set
+to 4444 so we can attempt to look for a Selenium server at
+C<127.0.0.1:4444>.
+
+=cut
+
+has '+port' => (
+    is => 'lazy',
+    builder => sub {
+        my ($self) = @_;
+
+        if ($self->binary) {
+            return find_open_port_above($self->binary_port);
+        }
+        else {
+            return '4444'
+        }
+    }
+);
+
+=attr binary_mode
+
+Mostly intended for internal use, its builder coordinates all the side
+effects of interacting with the binary: locating the executable,
+finding an open port, setting up the environment, shelling out to
+start the binary, and ensuring that the webdriver is listening on the
+correct port.
+
+If all of the above steps pass, it will return truthy after
+instantiation. If any of them fail, it should return falsy and the
+class should attempt normal L<Selenium::Remote::Driver> behavior.
+
+=cut
+
+has 'binary_mode' => (
+    is => 'lazy',
+    init_arg => undef,
+    builder => 1,
+    predicate => 1
+);
+
+has 'try_binary' => (
+    is => 'lazy',
+    default => sub { 0 },
+    trigger => sub {
+        my ($self) = @_;
+        $self->binary_mode if $self->try_binary;
+    }
+);
+
+=attr window_title
+
+Intended for internal use: this will build us a unique title for the
+background binary process of the Webdriver. Then, when we're cleaning
+up, we know what the window title is that we're going to C<taskkill>.
+
+=cut
+
+has 'window_title' => (
+    is => 'lazy',
+    init_arg => undef,
+    builder => sub {
+        my ($self) = @_;
+        my (undef, undef, $file) = File::Spec->splitpath( $self->binary );
+        my $port = $self->port;
+
+        return $file . ':' . $port;
+    }
+);
+
+use constant IS_WIN => $^O eq 'MSWin32';
+
+sub BUILDARGS {
+    # There's a bit of finagling to do to since we can't ensure the
+    # attribute instantiation order. To decide whether we're going into
+    # binary mode, we need the remote_server_addr and port. But, they're
+    # both lazy and only instantiated immediately before S:R:D's
+    # remote_conn attribute. Once remote_conn is set, we can't change it,
+    # so we need the following order:
+    #
+    #     parent: remote_server_addr, port
+    #     role:   binary_mode (aka _build_binary_mode)
+    #     parent: remote_conn
+    #
+    # Since we can't force an order, we introduced try_binary which gets
+    # decided during BUILDARGS to tip us off as to whether we should try
+    # binary mode or not.
+    my ( $class, %args ) = @_;
+
+    if ( ! exists $args{remote_server_addr} && ! exists $args{port} ) {
+        $args{try_binary} = 1;
+
+        # Windows may throw a fit about invalid pointers if we try to
+        # connect to localhost instead of 127.1
+        $args{remote_server_addr} = '127.0.0.1';
+    }
+
+    return { %args };
+}
+
+sub _build_binary_mode {
+    my ($self) = @_;
+
+    my $executable = $self->binary;
+    return unless $executable;
+
+    my $port = $self->port;
+    return unless $port != 4444;
+    if ($self->isa('Selenium::Firefox')) {
+        setup_firefox_binary_env($port);
+    }
+    my $command = $self->_construct_command($executable, $port);
+
+    system($command);
+    my $success = wait_until { probe_port($port) } timeout => 10;
+    if ($success) {
+        return 1;
+    }
+    else {
+        die 'Unable to connect to the ' . $executable . ' binary on port ' . $port;
+    }
+}
+
+sub shutdown_binary {
+    my ($self) = @_;
+
+    # TODO: Allow user to keep browser open after test
+    $self->quit;
+
+    if ($self->has_binary_mode && $self->binary_mode) {
+        my $port = $self->port;
+        my $ua = $self->ua;
+
+        $ua->get('127.0.0.1:' . $port . '/wd/hub/shutdown');
+
+        # Close the additional command windows on windows
+        if (IS_WIN) {
+            # Blech, handle a race condition that kills the driver
+            # before it's finished cleaning up its sessions
+            sleep(1);
+            $self->shutdown_windows_binary;
+        }
+    }
+}
+
+sub shutdown_windows_binary {
+    my ($self) = @_;
+
+    # Firefox doesn't have a Driver/Session architecture - the only
+    # thing running is Firefox itself, so there's no other task to
+    # kill.
+    return if $self->isa('Selenium::Firefox');
+
+    my $kill = 'taskkill /FI "WINDOWTITLE eq ' . $self->window_title . '"';
+    system($kill);
+}
+
+before DEMOLISH => sub {
+    my ($self) = @_;
+    $self->shutdown_binary;
+};
+
+sub DEMOLISH { };
+
+sub _construct_command {
+    my ($self, $executable, $port) = @_;
+
+    # Handle spaces in executable path names
+    $executable = '"' . $executable . '"';
+
+    my %args;
+    if ($executable =~ /chromedriver(\.exe)?"$/i) {
+        %args = (
+            port => $port,
+            'url-base' => 'wd/hub'
+        );
+    }
+    elsif ($executable =~ /phantomjs(\.exe)?"$/i) {
+        %args = (
+            webdriver => '127.0.0.1:' . $port
+        );
+    }
+    elsif ($executable =~ /firefox(-bin|\.exe)"$/i) {
+        $executable .= ' -no-remote ';
+    }
+
+    my @args = map { '--' . $_ . '=' . $args{$_} } keys %args;
+
+    # Handle Windows vs Unix discrepancies for invoking shell commands
+    my ($prefix, $suffix) = ($self->_cmd_prefix, $self->_cmd_suffix);
+    return join(' ', ($prefix, $executable, @args, $suffix) );
+}
+
+sub _cmd_prefix {
+    my ($self) = @_;
+
+    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 ';
+        }
+        return $prefix;
+    }
+    else {
+        return '';
+    }
+}
+
+sub _cmd_suffix {
+    # TODO: allow users to specify whether & where they want driver
+    # output to go
+
+    if (IS_WIN) {
+        return ' > /nul 2>&1 ';
+    }
+    else {
+        return ' > /dev/null 2>&1 &';
+    }
+}
+
+=head1 SEE ALSO
+
+Selenium::Chrome
+Selenium::Firefox
+Selenium::PhantomJS
+
+=cut
+
+1;

+ 68 - 0
lib/Selenium/CanStartBinary/FindBinary.pm

@@ -0,0 +1,68 @@
+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;
+use Selenium::Firefox::Binary qw/firefox_path/;
+
+require Exporter;
+our @ISA = qw/Exporter/;
+our @EXPORT_OK = qw/coerce_simple_binary coerce_firefox_binary/;
+
+sub coerce_simple_binary {
+    my ($executable) = @_;
+
+    my $manual_binary = _validate_manual_binary($executable);
+    if ($manual_binary) {
+        return $manual_binary;
+    }
+    else {
+        return _naive_find_binary($executable);
+    }
+}
+
+sub coerce_firefox_binary {
+    my ($executable) = @_;
+
+    my $manual_binary = _validate_manual_binary($executable);
+    if ($manual_binary) {
+        return $manual_binary;
+    }
+    else {
+        return firefox_path();
+    }
+}
+
+sub _validate_manual_binary {
+    my ($executable) = @_;
+
+    my $abs_executable = eval {
+        my $path = abs_path($executable);
+        die unless -e $path;
+        $path
+    };
+
+    if ( $abs_executable ) {
+        if ( -x $abs_executable ) {
+            return $abs_executable;
+        }
+        else {
+            die 'The binary at ' . $executable . ' is not executable. Choose the correct file or chmod +x it as needed.';
+        }
+    }
+}
+
+sub _naive_find_binary {
+    my ($executable) = @_;
+
+    my $naive_binary = which($executable);
+    if (defined $naive_binary) {
+        return $naive_binary;
+    }
+    else {
+        warn qq(Unable to find the $naive_binary binary in your \$PATH. We'll try falling back to standard Remote Driver);
+        return;
+    }
+}

+ 35 - 0
lib/Selenium/CanStartBinary/ProbePort.pm

@@ -0,0 +1,35 @@
+package Selenium::CanStartBinary::ProbePort;
+
+# ABSTRACT: Utility functions for finding open ports to eventually bind to
+use IO::Socket::INET;
+use Selenium::Waiter qw/wait_until/;
+
+require Exporter;
+our @ISA = qw/Exporter/;
+our @EXPORT_OK = qw/find_open_port_above probe_port/;
+
+sub find_open_port_above {
+    my ($port) = @_;
+
+    my $free_port = wait_until {
+        if ( probe_port($port) ) {
+            $port++;
+            return 0;
+        }
+        else {
+            return $port;
+        }
+    };
+
+    return $free_port;
+}
+
+sub probe_port {
+    my ($port) = @_;
+
+    return IO::Socket::INET->new(
+        PeerAddr => '127.0.0.1',
+        PeerPort => $port,
+        Timeout => 3
+    );
+}

+ 56 - 1
lib/Selenium/Chrome.pm

@@ -1,13 +1,33 @@
 package Selenium::Chrome;
 
-# ABSTRACT: A convenience package for creating a Chrome instance
+# ABSTRACT: Use ChromeDriver without a Selenium server
 use Moo;
+use Selenium::CanStartBinary::FindBinary qw/coerce_simple_binary/;
 extends 'Selenium::Remote::Driver';
 
 =head1 SYNOPSIS
 
     my $driver = Selenium::Chrome->new;
 
+=head1 DESCRIPTION
+
+This class allows you to use the ChromeDriver 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
+chromedriver executable binary in your $PATH. We'll try to start the
+binary connect to it, shutting it down at the end of the test.
+
+If the chromedriver binary is not found, 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're curious whether your Selenium::Chrome instance is using a
+separate ChromeDriver binary, or through the selenium server, you can
+check the C<binary_mode> attr after instantiation.
+
 =cut
 
 has '+browser_name' => (
@@ -15,4 +35,39 @@ has '+browser_name' => (
     default => sub { 'chrome' }
 );
 
+=attr binary
+
+Optional: specify the path to your binary. If you don't specify
+anything, we'll try to find it on our own via L<File::Which/which>.
+
+=cut
+
+has 'binary' => (
+    is => 'lazy',
+    coerce => \&coerce_simple_binary,
+    default => sub { 'chromedriver' },
+    predicate => 1
+);
+
+=attr binary_port
+
+Optional: specify the port that we should bind to. If you don't
+specify anything, we'll default to the driver's default port. Since
+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.
+
+=cut
+
+has 'binary_port' => (
+    is => 'lazy',
+    default => sub { 9515 }
+);
+
+with 'Selenium::CanStartBinary';
+
 1;

+ 59 - 2
lib/Selenium/Firefox.pm

@@ -1,12 +1,32 @@
 package Selenium::Firefox;
 
-# ABSTRACT: A convenience package for creating a Firefox instance
+# ABSTRACT: Use FirefoxDriver without a Selenium server
 use Moo;
+use Selenium::CanStartBinary::FindBinary qw/coerce_firefox_binary/;
 extends 'Selenium::Remote::Driver';
 
 =head1 SYNOPSIS
 
-    my $driver = Selenium::Firefox->new;
+my $driver = Selenium::Firefox->new;
+
+=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.
+
+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'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.
 
 =cut
 
@@ -15,4 +35,41 @@ has '+browser_name' => (
     default => sub { 'firefox' }
 );
 
+=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.
+
+=cut
+
+has 'binary' => (
+    is => 'lazy',
+    coerce => \&coerce_firefox_binary,
+    default => sub { 'firefox' },
+    predicate => 1
+);
+
+=attr binary_port
+
+Optional: specify the port that we should bind to. If you don't
+specify anything, we'll default to the driver's default port. Since
+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.
+
+=cut
+
+has 'binary_port' => (
+    is => 'lazy',
+    default => sub { 9090 }
+);
+
+with 'Selenium::CanStartBinary';
+
 1;

+ 80 - 0
lib/Selenium/Firefox/Binary.pm

@@ -0,0 +1,80 @@
+package Selenium::Firefox::Binary;
+
+# ABSTRACT: Subroutines for locating and properly initializing the Firefox Binary
+use File::Which qw/which/;
+use Selenium::Firefox::Profile;
+
+require Exporter;
+our @ISA = qw/Exporter/;
+our @EXPORT_OK = qw/firefox_path setup_firefox_binary_env/;
+
+sub _firefox_windows_path {
+    # TODO: make this slightly less dumb
+    my @program_files = (
+        $ENV{PROGRAMFILES} // 'C:\Program Files',
+        $ENV{'PROGRAMFILES(X86)'} // 'C:\Program Files (x86)',
+    );
+
+    foreach (@program_files) {
+        my $binary_path = $_ . '\Mozilla Firefox\firefox.exe';
+        return $binary_path if -x $binary_path;
+    }
+
+    # Fall back to a completely naive strategy
+    warn q/We couldn't find a viable firefox.EXE; you may want to specify it via the binary attribute./;
+    return which('firefox');
+}
+
+sub _firefox_darwin_path {
+    my $default_firefox = '/Applications/Firefox.app/Contents/MacOS/firefox-bin';
+
+    if (-e $default_firefox && -x $default_firefox) {
+        return $default_firefox
+    }
+    else {
+        return which('firefox-bin');
+    }
+}
+
+sub _firefox_unix_path {
+    # TODO: maybe which('firefox3'), which('firefox2') ?
+    return which('firefox') || '/usr/bin/firefox';
+}
+
+sub firefox_path {
+    my $path;
+    if ($^O eq 'MSWin32') {
+        $path =_firefox_windows_path();
+    }
+    elsif ($^O eq 'darwin') {
+        $path = _firefox_darwin_path();
+    }
+    else {
+        $path = _firefox_unix_path;
+    }
+
+    if (not -x $path) {
+        die $path . ' is not an executable file.';
+    }
+
+    return $path;
+}
+
+# We want the profile to persist to the end of the session, not just
+# the end of this function.
+my $profile;
+sub setup_firefox_binary_env {
+    my ($port) = @_;
+
+    # TODO: respect the user's profile instead of overwriting it
+    $profile = Selenium::Firefox::Profile->new;
+    $profile->add_webdriver($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
+}
+
+
+1;

+ 285 - 0
lib/Selenium/Firefox/Profile.pm

@@ -0,0 +1,285 @@
+package Selenium::Firefox::Profile;
+
+# ABSTRACT: Use custom profiles with Selenium::Remote::Driver
+
+use strict;
+use warnings;
+
+use Archive::Zip qw( :ERROR_CODES );
+use Carp qw(croak);
+use Cwd qw(abs_path);
+use File::Copy qw(copy);
+use File::Temp;
+use File::Basename qw(dirname);
+use IO::Uncompress::Unzip qw(unzip $UnzipError);
+use JSON qw/decode_json/;
+use MIME::Base64;
+use Scalar::Util qw(blessed looks_like_number);
+use XML::Simple;
+
+=head1 DESCRIPTION
+
+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.
+
+=head1 SYNPOSIS
+
+    use Selenium::Remote::Driver;
+    use Selenium::Firefox::Profile;
+
+    my $profile = Selenium::Firefox::Profile->new;
+    $profile->set_preference(
+        'browser.startup.homepage' => 'http://www.google.com',
+        'browser.cache.disk.capacity' => 358400
+    );
+
+    $profile->set_boolean_preference(
+        'browser.shell.checkDefaultBrowser' => 0
+    );
+
+    $profile->add_extension('t/www/redisplay.xpi');
+
+    my $driver = Selenium::Remote::Driver->new(
+        'firefox_profile' => $profile
+    );
+
+    $driver->get('http://www.google.com');
+    print $driver->get_title();
+
+=cut
+
+sub new {
+    my $class = shift;
+
+    # TODO: add handling for a pre-existing profile folder passed into
+    # the constructor
+
+    # TODO: accept user prefs, boolean prefs, and extensions in
+    # constructor
+    my $self = {
+        profile_dir => File::Temp->newdir(),
+        user_prefs => {},
+        extensions => []
+      };
+    bless $self, $class or die "Can't bless $class: $!";
+
+    return $self;
+}
+
+=method set_preference
+
+Set string and integer preferences on the profile object. You can set
+multiple preferences at once. If you need to set a boolean preference,
+either use JSON::true/JSON::false, or see C<set_boolean_preference()>.
+
+    $profile->set_preference("quoted.integer.pref" => '"20140314220517"');
+    # user_pref("quoted.integer.pref", "20140314220517");
+
+    $profile->set_preference("plain.integer.pref" => 9005);
+    # user_pref("plain.integer.pref", 9005);
+
+    $profile->set_preference("string.pref" => "sample string value");
+    # user_pref("string.pref", "sample string value");
+
+=cut
+
+sub set_preference {
+    my ($self, %prefs) = @_;
+
+    foreach (keys %prefs) {
+        my $value = $prefs{$_};
+        my $clean_value = '';
+
+        if ( JSON::is_bool($value) ) {
+            $self->set_boolean_preference($_, $value );
+            next;
+        }
+        elsif ($value =~ /^(['"]).*\1$/ or looks_like_number($value)) {
+            # plain integers: 0, 1, 32768, or integers wrapped in strings:
+            # "0", "1", "20140204". in either case, there's nothing for us
+            # to do.
+            $clean_value = $value;
+        }
+        else {
+            # otherwise it's hopefully a string that we'll need to
+            # quote on our own
+            $clean_value = '"' . $value . '"';
+        }
+
+        $self->{user_prefs}->{$_} = $clean_value;
+    }
+}
+
+=method set_boolean_preference
+
+Set preferences that require boolean values of 'true' or 'false'. You
+can set multiple preferences at once. For string or integer
+preferences, use C<set_preference()>.
+
+    $profile->set_boolean_preference("false.pref" => 0);
+    # user_pref("false.pref", false);
+
+    $profile->set_boolean_preference("true.pref" => 1);
+    # user_pref("true.pref", true);
+
+=cut
+
+sub set_boolean_preference {
+    my ($self, %prefs) = @_;
+
+    foreach (keys %prefs) {
+        my $value = $prefs{$_};
+
+        $self->{user_prefs}->{$_} = $value ? 'true' : 'false';
+    }
+}
+
+=method get_preference
+
+Retrieve the computed value of a preference. Strings will be double
+quoted and boolean values will be single quoted as "true" or "false"
+accordingly.
+
+    $profile->set_boolean_preference("true.pref" => 1);
+    print $profile->get_preference("true.pref") # true
+
+    $profile->set_preference("string.pref" => "an extra set of quotes");
+    print $profile->get_preference("string.pref") # "an extra set of quotes"
+
+=cut
+
+sub get_preference {
+    my ($self, $pref) = @_;
+
+    return $self->{user_prefs}->{$pref};
+}
+
+=method add_extension
+
+Add an existing C<.xpi> to the profile by providing its path. This
+only works with packaged C<.xpi> files, not plain/un-packed extension
+directories.
+
+    $profile->add_extension('t/www/redisplay.xpi');
+
+=cut
+
+sub add_extension {
+    my ($self, $xpi) = @_;
+
+    croak 'File not found: ' . $xpi unless -e $xpi;
+    my $xpi_abs_path = abs_path($xpi);
+    croak '$xpi_abs_path: extensions must be in .xpi format' unless $xpi_abs_path =~ /\.xpi$/;
+
+    push (@{$self->{extensions}}, $xpi_abs_path);
+}
+
+=method add_webdriver
+
+Primarily for internal use, we add the webdriver extension to the
+current Firefox profile.
+
+=cut
+
+sub add_webdriver {
+    my ($self, $port) = @_;
+
+    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;
+    {
+        local $/;
+        open (my $fh, '<', $default_prefs_filename);
+        $json = <$fh>;
+        close ($fh);
+    }
+    my $webdriver_prefs = decode_json($json);
+
+    # TODO: Let the user's mutable preferences persist instead of
+    # overwriting them here.
+    $self->set_preference(%{ $webdriver_prefs->{mutable} });
+    $self->set_preference(%{ $webdriver_prefs->{frozen} });
+
+    $self->add_extension($webdriver_extension);
+    $self->set_preference('webdriver_firefox_port', $port);
+}
+
+sub _encode {
+    my $self = shift;
+
+    # The remote webdriver accepts the Firefox profile as a base64
+    # encoded zip file
+    $self->_layout_on_disk();
+
+    my $zip = Archive::Zip->new();
+    my $dir_member = $zip->addTree( $self->{profile_dir} );
+
+    my $string = "";
+    open (my $fh, ">", \$string);
+    binmode($fh);
+    unless ( $zip->writeToFileHandle($fh) == AZ_OK ) {
+        die 'write error';
+    }
+
+    return encode_base64($string);
+}
+
+sub _layout_on_disk {
+    my $self = shift;
+
+    $self->_write_preferences();
+    $self->_install_extensions();
+
+    return $self->{profile_dir};
+}
+
+sub _write_preferences {
+    my $self = shift;
+
+    my $userjs = $self->{profile_dir} . "/user.js";
+    open (my $fh, ">>", $userjs)
+        or die "Cannot open $userjs for writing preferences: $!";
+
+    foreach (keys %{$self->{user_prefs}}) {
+        print $fh 'user_pref("' . $_ . '", ' . $self->get_preference($_) . ');' . "\n";
+    }
+    close ($fh);
+}
+
+sub _install_extensions {
+    my $self = shift;
+    my $extension_dir = $self->{profile_dir} . "/extensions/";
+    mkdir $extension_dir unless -d $extension_dir;
+
+    # TODO: handle extensions that need to be unpacked
+    foreach my $xpi (@{$self->{extensions}}) {
+        # For Firefox to recognize the extension, we have to put the
+        # .xpi in the /extensions/ folder and change the filename to
+        # its id, which is found in the install.rdf in the root of the
+        # zip.
+
+        my $fh;
+        unzip $xpi => \$fh, Name => "install.rdf"
+          or die "unzip failed: $UnzipError\n";
+
+        my $rdf = XMLin($fh);
+        my $name = $rdf->{Description}->{'em:id'};
+
+        my $xpi_dest = $extension_dir . $name . ".xpi";
+        copy($xpi, $xpi_dest)
+            or croak "Error copying $_ to $xpi_dest : $!";
+    }
+}
+
+1;
+
+__END__
+
+=head1 SEE ALSO
+
+http://kb.mozillazine.org/About:config_entries
+https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/A_brief_guide_to_Mozilla_preferences

BIN
lib/Selenium/Firefox/amd64/libibushandler.so


BIN
lib/Selenium/Firefox/amd64/x_ignore_nofocus.so


BIN
lib/Selenium/Firefox/webdriver.xpi


+ 68 - 0
lib/Selenium/Firefox/webdriver_prefs.json

@@ -0,0 +1,68 @@
+{
+  "frozen": {
+    "app.update.auto": false,
+    "app.update.enabled": false,
+    "browser.download.manager.showWhenStarting": false,
+    "browser.EULA.override": true,
+    "browser.EULA.3.accepted": true,
+    "browser.link.open_external": 2,
+    "browser.link.open_newwindow": 2,
+    "browser.offline": false,
+    "browser.safebrowsing.enabled": false,
+    "browser.safebrowsing.malware.enabled": false,
+    "browser.search.update": false,
+    "browser.sessionstore.resume_from_crash": false,
+    "browser.shell.checkDefaultBrowser": false,
+    "browser.tabs.warnOnClose": false,
+    "browser.tabs.warnOnOpen": false,
+    "datareporting.healthreport.service.enabled": false,
+    "datareporting.healthreport.uploadEnabled": false,
+    "datareporting.healthreport.service.firstRun": false,
+    "datareporting.healthreport.logging.consoleEnabled": false,
+    "datareporting.policy.dataSubmissionEnabled": false,
+    "datareporting.policy.dataSubmissionPolicyAccepted": false,
+    "devtools.errorconsole.enabled": true,
+    "dom.disable_open_during_load": false,
+    "extensions.autoDisableScopes": 10,
+    "extensions.blocklist.enabled": false,
+    "extensions.logging.enabled": true,
+    "extensions.update.enabled": false,
+    "extensions.update.notifyUser": false,
+    "network.manage-offline-status": false,
+    "network.http.phishy-userpass-length": 255,
+    "offline-apps.allow_by_default": true,
+    "prompts.tab_modal.enabled": false,
+    "security.csp.enable": false,
+    "security.fileuri.origin_policy": 3,
+    "security.fileuri.strict_origin_policy": false,
+    "security.warn_entering_secure": false,
+    "security.warn_entering_secure.show_once": false,
+    "security.warn_entering_weak": false,
+    "security.warn_entering_weak.show_once": false,
+    "security.warn_leaving_secure": false,
+    "security.warn_leaving_secure.show_once": false,
+    "security.warn_submit_insecure": false,
+    "security.warn_viewing_mixed": false,
+    "security.warn_viewing_mixed.show_once": false,
+    "signon.rememberSignons": false,
+    "toolkit.networkmanager.disable": true,
+    "toolkit.telemetry.prompted": 2,
+    "toolkit.telemetry.enabled": false,
+    "toolkit.telemetry.rejected": true
+  },
+  "mutable": {
+    "browser.dom.window.dump.enabled": true,
+    "browser.newtab.url": "about:blank",
+    "browser.newtabpage.enabled": false,
+    "browser.startup.page": 0,
+    "browser.startup.homepage": "about:blank",
+    "dom.max_chrome_script_run_time": 30,
+    "dom.max_script_run_time": 30,
+    "dom.report_all_js_exceptions": true,
+    "javascript.options.showInConsole": true,
+    "network.http.max-connections-per-server": 10,
+    "startup.homepage_welcome_url": "about:blank",
+    "webdriver_accept_untrusted_certs": true,
+    "webdriver_assume_untrusted_issuer": true
+  }
+}

BIN
lib/Selenium/Firefox/x86/libibushandler.so


BIN
lib/Selenium/Firefox/x86/x_ignore_nofocus.so


+ 56 - 1
lib/Selenium/PhantomJS.pm

@@ -1,13 +1,33 @@
 package Selenium::PhantomJS;
 
-# ABSTRACT: A convenience package for creating a PhantomJS instance
+# ABSTRACT: Use GhostDriver without a Selenium server
 use Moo;
+use Selenium::CanStartBinary::FindBinary qw/coerce_simple_binary/;
 extends 'Selenium::Remote::Driver';
 
 =head1 SYNOPSIS
 
     my $driver = Selenium::PhantomJS->new;
 
+=head1 DESCRIPTION
+
+This class allows you to use PhantomJS via Ghostdriver 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 phantomjs executable binary in your $PATH. We'll try to start the
+binary connect to it, shutting it down at the end of the test.
+
+If the binary is not found, 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're curious whether your Selenium::PhantomJS instance is using a
+separate PhantomJS binary, or through the selenium server, you can check
+the C<binary_mode> attr after instantiation.
+
 =cut
 
 has '+browser_name' => (
@@ -15,4 +35,39 @@ has '+browser_name' => (
     default => sub { 'phantomjs' }
 );
 
+=attr binary
+
+Optional: specify the path to your binary. If you don't specify
+anything, we'll try to find it on our own via L<File::Which/which>.
+
+=cut
+
+has 'binary' => (
+    is => 'lazy',
+    coerce => \&coerce_simple_binary,
+    default => sub { 'phantomjs' },
+    predicate => 1
+);
+
+=attr binary_port
+
+Optional: specify the port that we should bind to. If you don't
+specify anything, we'll default to the driver's default port. Since
+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.
+
+=cut
+
+has 'binary_port' => (
+    is => 'lazy',
+    default => sub { 8910 }
+);
+
+with 'Selenium::CanStartBinary';
+
 1;

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

@@ -128,7 +128,7 @@ available here.
         'platform'             - <string>   - desired platform: {WINDOWS|XP|VISTA|MAC|LINUX|UNIX|ANY}
         'javascript'           - <boolean>  - whether javascript should be supported
         'accept_ssl_certs'     - <boolean>  - whether SSL certs should be accepted, default is true.
-        'firefox_profile'      - Profile    - Use S::R::D::Firefox::Profile to create a Firefox profile for the browser to use
+        'firefox_profile'      - Profile    - Use Selenium::Firefox::Profile to create a Firefox profile for the browser to use
         'proxy'                - HASH       - Proxy configuration with the following keys:
             'proxyType' - <string> - REQUIRED, Possible values are:
                 direct     - A direct connection - no proxy in use,
@@ -260,6 +260,7 @@ has 'remote_server_addr' => (
     is      => 'rw',
     coerce  => sub { ( defined($_[0]) ? $_[0] : 'localhost' )},
     default => sub {'localhost'},
+    predicate => 1
 );
 
 has 'browser_name' => (
@@ -289,6 +290,7 @@ has 'port' => (
     is      => 'rw',
     coerce  => sub { ( defined($_[0]) ? $_[0] : '4444' )},
     default => sub {'4444'},
+    predicate => 1
 );
 
 has 'version' => (
@@ -386,8 +388,8 @@ has 'firefox_profile' => (
     coerce    => sub {
         my $profile = shift;
         unless (Scalar::Util::blessed($profile)
-          && $profile->isa('Selenium::Remote::Driver::Firefox::Profile')) {
-            croak "firefox_profile should be a Selenium::Remote::Driver::Firefox::Profile\n";
+          && $profile->isa('Selenium::Firefox::Profile')) {
+            croak "firefox_profile should be a Selenium::Firefox::Profile\n";
         }
 
         return $profile->_encode;

+ 8 - 236
lib/Selenium/Remote/Driver/Firefox/Profile.pm

@@ -1,254 +1,26 @@
 package Selenium::Remote::Driver::Firefox::Profile;
 
 # ABSTRACT: Use custom profiles with Selenium::Remote::Driver
-
 use strict;
 use warnings;
+use Selenium::Firefox::Profile;
 
-use Archive::Zip qw( :ERROR_CODES );
-use Archive::Extract;
-use Carp qw(croak);
-use Cwd qw(abs_path);
-use File::Copy qw(copy);
-use File::Temp;
-use MIME::Base64;
-use Scalar::Util qw(looks_like_number);
-
-=head1 DESCRIPTION
-
-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.
-
-=head1 SYNPOSIS
-
-    use Selenium::Remote::Driver;
-    use Selenium::Remote::Driver::Firefox::Profile;
-
-    my $profile = Selenium::Remote::Driver::Firefox::Profile->new;
-    $profile->set_preference(
-        'browser.startup.homepage' => 'http://www.google.com',
-        'browser.cache.disk.capacity' => 358400
-    );
-
-    $profile->set_boolean_preference(
-        'browser.shell.checkDefaultBrowser' => 0
-    );
-
-    $profile->add_extension('t/www/redisplay.xpi');
-
-    my $driver = Selenium::Remote::Driver->new(
-        'firefox_profile' => $profile
-    );
-
-    $driver->get('http://www.google.com');
-    print $driver->get_title();
-
-=cut
-
-sub new {
-    my $class = shift;
-
-    # TODO: add handling for a pre-existing profile folder passed into
-    # the constructor
-
-    # TODO: accept user prefs, boolean prefs, and extensions in
-    # constructor
-    my $self = {
-        profile_dir => File::Temp->newdir(),
-        user_prefs => {},
-        extensions => []
-      };
-    bless $self, $class or die "Can't bless $class: $!";
-
-    return $self;
-}
-
-=method set_preference
-
-Set string and integer preferences on the profile object. You can set
-multiple preferences at once. If you need to set a boolean preference,
-see C<set_boolean_preference()>.
-
-    $profile->set_preference("quoted.integer.pref" => '"20140314220517"');
-    # user_pref("quoted.integer.pref", "20140314220517");
-
-    $profile->set_preference("plain.integer.pref" => 9005);
-    # user_pref("plain.integer.pref", 9005);
-
-    $profile->set_preference("string.pref" => "sample string value");
-    # user_pref("string.pref", "sample string value");
-
-=cut
-
-sub set_preference {
-    my ($self, %prefs) = @_;
-
-    foreach (keys %prefs) {
-        my $value = $prefs{$_};
-        my $clean_value = '';
-
-        if ($value =~ /^(['"]).*\1$/ or looks_like_number($value)) {
-            # plain integers: 0, 1, 32768, or integers wrapped in strings:
-            # "0", "1", "20140204". in either case, there's nothing for us
-            # to do.
-            $clean_value = $value;
-        }
-        else {
-            # otherwise it's hopefully a string that we'll need to
-            # quote on our own
-            $clean_value = '"' . $value . '"';
-        }
-
-        $self->{user_prefs}->{$_} = $clean_value;
-    }
-}
-
-=method set_boolean_preference
-
-Set preferences that require boolean values of 'true' or 'false'. You
-can set multiple preferences at once. For string or integer
-preferences, use C<set_preference()>.
-
-    $profile->set_boolean_preference("false.pref" => 0);
-    # user_pref("false.pref", false);
-
-    $profile->set_boolean_preference("true.pref" => 1);
-    # user_pref("true.pref", true);
-
-=cut
-
-sub set_boolean_preference {
-    my ($self, %prefs) = @_;
-
-    foreach (keys %prefs) {
-        my $value = $prefs{$_};
-
-        $self->{user_prefs}->{$_} = $value ? 'true' : 'false';
-    }
-}
-
-=method get_preference
-
-Retrieve the computed value of a preference. Strings will be double
-quoted and boolean values will be single quoted as "true" or "false"
-accordingly.
-
-    $profile->set_boolean_preference("true.pref" => 1);
-    print $profile->get_preference("true.pref") # true
-
-    $profile->set_preference("string.pref" => "an extra set of quotes");
-    print $profile->get_preference("string.pref") # "an extra set of quotes"
-
-=cut
-
-sub get_preference {
-    my ($self, $pref) = @_;
-
-    return $self->{user_prefs}->{$pref};
+BEGIN {
+    push our @ISA, 'Selenium::Firefox::Profile';
 }
 
-=method add_extension
-
-Add an existing C<.xpi> to the profile by providing its path. This
-only works with packaged C<.xpi> files, not plain/un-packed extension
-directories.
+=head1 DESCRIPTION
 
-    $profile->add_extension('t/www/redisplay.xpi');
+We've renamed this class to the slightly less wordy
+L<Selenium::Firefox::Profile>. This is only around as an alias to
+hopefully prevent old code from breaking.
 
 =cut
 
-sub add_extension {
-    my ($self, $xpi) = @_;
-
-    croak 'File not found: ' . $xpi unless -e $xpi;
-    my $xpi_abs_path = abs_path($xpi);
-    croak '$xpi_abs_path: extensions must be in .xpi format' unless $xpi_abs_path =~ /\.xpi$/;
-
-    push (@{$self->{extensions}}, $xpi_abs_path);
-}
-
-sub _encode {
-    my $self = shift;
-
-    # The remote webdriver accepts the Firefox profile as a base64
-    # encoded zip file
-    $self->_layout_on_disk();
-
-    my $zip = Archive::Zip->new();
-    my $dir_member = $zip->addTree( $self->{profile_dir} );
-
-    my $string = "";
-    open (my $fh, ">", \$string);
-    binmode($fh);
-    unless ( $zip->writeToFileHandle($fh) == AZ_OK ) {
-        die 'write error';
-    }
-
-    return encode_base64($string);
-}
-
-sub _layout_on_disk {
-    my $self = shift;
-
-    $self->_write_preferences();
-    $self->_install_extensions();
-
-    return $self->{profile_dir};
-}
-
-sub _write_preferences {
-    my $self = shift;
-
-    my $userjs = $self->{profile_dir} . "/user.js";
-    open (my $fh, ">>", $userjs)
-        or die "Cannot open $userjs for writing preferences: $!";
-
-    foreach (keys %{$self->{user_prefs}}) {
-        print $fh 'user_pref("' . $_ . '", ' . $self->get_preference($_) . ');' . "\n";
-    }
-    close ($fh);
-}
-
-sub _install_extensions {
-    my $self = shift;
-    my $extension_dir = $self->{profile_dir} . "/extensions/";
-    mkdir $extension_dir unless -d $extension_dir;
-
-    # TODO: handle extensions that need to be unpacked
-    foreach (@{$self->{extensions}}) {
-        # For Firefox to recognize the extension, we have to put the
-        # .xpi in the /extensions/ folder and change the filename to
-        # its id, which is found in the install.rdf in the root of the
-        # zip.
-        my $ae = Archive::Extract->new( archive => $_,
-                                        type => "zip");
-
-        my $tempDir = File::Temp->newdir();
-        $ae->extract( to => $tempDir );
-        my $install = $ae->extract_path();
-        $install .= '/install.rdf';
-
-        open (my $fh, "<", $install)
-            or croak "No install.rdf inside $_: $!";
-        my (@file) = <$fh>;
-        close ($fh);
-
-        my @name = grep { chomp; $_ =~ /<em:id>[^{]/ } @file;
-        $name[0] =~ s/.*<em:id>(.*)<\/em:id>.*/$1/;
-
-        my $xpi_dest = $extension_dir . $name[0] . ".xpi";
-        copy($_, $xpi_dest)
-            or croak "Error copying $_ to $xpi_dest : $!";
-    }
-}
-
 1;
 
-__END__
-
 =head1 SEE ALSO
 
+Selenium::Firefox::Profile
 http://kb.mozillazine.org/About:config_entries
 https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/A_brief_guide_to_Mozilla_preferences

+ 89 - 0
t/CanStartBinary.t

@@ -0,0 +1,89 @@
+#! /usr/bin/perl
+
+use strict;
+use warnings;
+use File::Which qw/which/;
+use Selenium::Chrome;
+use Selenium::Firefox;
+use Selenium::Firefox::Binary;
+use Selenium::PhantomJS;
+use Test::Fatal;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan skip_all => "Author tests not required for installation.";
+}
+
+PHANTOMJS: {
+  SKIP: {
+        my $has_phantomjs = which('phantomjs');
+        skip 'Phantomjs binary not found in path', 3
+          unless $has_phantomjs;
+
+        skip 'PhantomJS binary not found in path', 3
+          unless is_proper_phantomjs_available();
+
+        my $phantom = Selenium::PhantomJS->new;
+        is( $phantom->browser_name, 'phantomjs', 'binary phantomjs is okay');
+        isnt( $phantom->port, 4444, 'phantomjs can start up its own binary');
+
+        ok( Selenium::CanStartBinary::probe_port( $phantom->port ), 'the phantomjs binary is listening on its port');
+    }
+}
+
+MANUAL: {
+    ok( exception { PhantomJS->new( binary => '/bad/executable') },
+        'we throw if the user specified binary is not executable');
+
+  SKIP: {
+        my $phantom_binary = which('phantomjs');
+        skip 'PhantomJS needed for manual binary path tests', 2
+          unless $phantom_binary;
+
+        my $manual_phantom = Selenium::PhantomJS->new(
+            binary => $phantom_binary
+        );
+        isnt( $manual_phantom->port, 4444, 'manual phantom can start up user specified binary');
+        ok( Selenium::CanStartBinary::probe_port( $manual_phantom->port ), 'the manual chrome binary is listening on its port');
+    }
+}
+
+CHROME: {
+  SKIP: {
+        my $has_chromedriver = which('chromedriver');
+        skip 'Chrome binary not found in path', 3
+          unless $has_chromedriver;
+
+        my $chrome = Selenium::Chrome->new;
+        ok( $chrome->browser_name eq 'chrome', 'convenience chrome is okay' );
+        isnt( $chrome->port, 4444, 'chrome can start up its own binary');
+
+        ok( Selenium::CanStartBinary::probe_port( $chrome->port ), 'the chrome binary is listening on its port');
+    }
+}
+
+FIREFOX: {
+  SKIP: {
+        skip 'Firefox will not start up on UNIX without a display', 3
+          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');
+    }
+}
+
+sub is_proper_phantomjs_available {
+    my $ver = `phantomjs --version` // '';
+    chomp $ver;
+
+    $ver =~ s/^(\d\.\d).*/$1/;
+    return $ver >= 1.9;
+}
+
+done_testing;

+ 13 - 13
t/Firefox-Profile.t

@@ -7,7 +7,7 @@ use Selenium::Remote::Driver;
 use Test::More;
 
 use MIME::Base64 qw/decode_base64/;
-use Archive::Extract;
+use IO::Uncompress::Unzip qw(unzip $UnzipError);
 use File::Temp;
 use JSON;
 use Selenium::Remote::Mock::RemoteConnection;
@@ -126,6 +126,15 @@ PREFERENCES: {
             cmp_ok($profile->get_preference($_), "eq", $expected->{$_},
                    "$_ pref is formatted correctly");
         }
+
+        $profile->set_preference(
+            'boolean.true.2' => JSON::true,
+            'boolean.false.2' => JSON::false
+        );
+        is($profile->get_preference('boolean.true.2'), 'true',
+           'format true booleans via set_preference & JSON::true');
+        is($profile->get_preference('boolean.false.2'), 'false',
+           'format false booleans via set_preference & JSON::false');
     }
 
   PACK_AND_UNPACK: {
@@ -133,19 +142,10 @@ PREFERENCES: {
         my $fh = File::Temp->new();
         print $fh decode_base64($encoded);
         close $fh;
-        my $zip = Archive::Extract->new(
-            archive => $fh->filename,
-            type => "zip"
-        );
-        my $tempdir = File::Temp->newdir();
-        my $ok = $zip->extract( to => $tempdir );
-        my $outdir = $zip->extract_path;
 
-        my $filename = $tempdir . "/user.js";
-        open ($fh, "<", $filename);
-        my (@file) = <$fh>;
-        close ($fh);
-        my $userjs = join('', @file);
+        my $userjs;
+        unzip $fh->filename => \$userjs, Name => "user.js"
+          or die "unzip failed: $UnzipError\n";
 
         foreach (keys %$expected) {
             my $value = $expected->{$_};

+ 1 - 0
t/convenience.t

@@ -17,6 +17,7 @@ my $harness = TestHarness->new(
 );
 
 my %caps = %{ $harness->base_caps };
+$caps{remote_server_addr} = '127.0.0.1';
 delete $caps{browser_name};
 
 my $firefox = Selenium::Firefox->new( %caps );