Forráskód Böngészése

For #118 & #64 - restore Firefox::Profile from #84

Daniel Gempesaw 11 éve
szülő
commit
e12106d143

+ 27 - 0
lib/Selenium/Remote/Driver.pm

@@ -279,6 +279,28 @@ has 'proxy' => (
 has 'extra_capabilities' => (
     is      => 'rw',
     default => sub { {} },
+    trigger => sub {
+        my ($self, $extra_capabilities) = @_;
+
+        if (defined $extra_capabilities->{firefox_profile} && !$self->has_firefox_profile) {
+            $self->firefox_profile($extra_capabilities->{firefox_profile});
+        }
+    }
+);
+
+has 'firefox_profile' => (
+    is => 'rw',
+    init_arg => undef,
+    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";
+        }
+
+        return $profile->_encode;
+    },
+    predicate => 'has_firefox_profile'
 );
 
 sub BUILD {
@@ -359,6 +381,11 @@ sub new_session {
         $args->{desiredCapabilities}->{proxy} = $self->proxy;
     }
 
+    if ($args->{desiredCapabilities}->{browserName} =~ /firefox/i
+        && $self->has_firefox_profile) {
+        $args->{desiredCapabilities}->{firefox_profile} = $self->firefox_profile;
+    }
+
     # command => 'newSession' to fool the tests of commands implemented
     # TODO: rewrite the testing better, this is so fragile.
     my $resp = $self->remote_conn->request(

+ 183 - 0
lib/Selenium/Remote/Driver/Firefox/Profile.pm

@@ -0,0 +1,183 @@
+package Selenium::Remote::Driver::Firefox::Profile;
+
+# ABSTRACT: Use custom profiles with Selenium::Remote::Driver
+
+use strict;
+use warnings;
+
+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(blessed looks_like_number);
+
+sub new {
+    my $class = shift;
+
+    # TODO: add handling for a pre-existing profile folder passed into
+    # the constructor
+    my $self = {
+        profile_dir => File::Temp->newdir(),
+        user_prefs => {},
+        extensions => []
+      };
+    bless $self, $class or die "Can't bless $class: $!";
+
+    return $self;
+}
+
+sub set_preference {
+    my ($self, %prefs) = @_;
+
+    foreach (keys %prefs) {
+        my $value = $prefs{$_};
+        my $clean_value = '';
+
+        if (blessed($value) and $value->isa("JSON::Boolean")) {
+            $clean_value = $value ? "true" : "false";
+        }
+        elsif ($value =~ /^(['"]).*\1$/ or looks_like_number($value)) {
+            $clean_value = $value;
+        }
+        else {
+            $clean_value = '"' . $value . '"';
+        }
+
+        $self->{user_prefs}->{$_} = $clean_value;
+    }
+}
+
+sub set_preferences {
+    my ($self, %prefs) = @_;
+    $self->set_preference(%prefs);
+}
+
+sub get_preference {
+    my ($self, $pref) = @_;
+
+    return $self->{user_prefs}->{$pref};
+}
+
+sub add_extension {
+    my ($self, $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 path {
+    my $self = shift;
+    return $self->{profile_dir};
+}
+
+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 croak "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 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"
+       );
+
+    $profile->add_extension('t/www/redisplay.xpi');
+
+    my $driver = Selenium::Remote::Driver->new(
+        extra_capabilities => {
+            firefox_profile => $profile
+        });
+    $driver->get("http://www.google.com");
+
+    print $driver->get_title();
+
+
+=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.

+ 178 - 0
t/Firefox-Profile.t

@@ -0,0 +1,178 @@
+#! /usr/bin/perl
+
+use strict;
+use warnings;
+
+use Selenium::Remote::Driver;
+use Test::More;
+
+use MIME::Base64 qw/decode_base64/;
+use Archive::Extract;
+use File::Temp;
+use JSON;
+
+BEGIN {
+    unless (use_ok('Selenium::Remote::Driver::Firefox::Profile')) {
+        BAIL_OUT ("Couldn't load Firefox Profile");
+        exit;
+    }
+
+    if (defined $ENV{'WD_MOCKING_RECORD'} && ($ENV{'WD_MOCKING_RECORD'}==1))
+    {
+        use t::lib::MockSeleniumWebDriver;
+        my $p = Net::Ping->new("tcp", 2);
+        $p->port_number(4444);
+        unless ($p->ping('localhost')) {
+            plan skip_all => "Selenium server is not running on localhost:4444";
+            exit;
+        }
+        warn "\n\nRecording...\n\n";
+    }
+}
+
+my $record = (defined $ENV{'WD_MOCKING_RECORD'} && ($ENV{'WD_MOCKING_RECORD'}==1))?1:0;
+my $os  = $^O;
+if ($os =~ m/(aix|freebsd|openbsd|sunos|solaris)/)
+{
+    $os = 'linux';
+}
+my $mock_file = "firefox-profile-mock-$os.json";
+if (!$record && !(-e "t/mock-recordings/$mock_file"))
+{
+    plan skip_all => "Mocking of tests is not been enabled for this platform";
+}
+t::lib::MockSeleniumWebDriver::register($record,"t/mock-recordings/$mock_file");
+
+# Start our local http server
+if ($^O eq 'MSWin32' && $record)
+{
+    system("start \"TEMP_HTTP_SERVER\" /MIN perl t/http-server.pl");
+} elsif ($record)
+{
+    system("perl t/http-server.pl > /dev/null &");
+}
+
+CUSTOM_EXTENSION_LOADED: {
+    my $profile = Selenium::Remote::Driver::Firefox::Profile->new();
+    $profile->set_preferences(
+        "browser.startup.homepage" => "http://www.google.com"
+    );
+
+    # This extension rewrites any page url that matches *.com to a
+    # single <h1>. The following javascript is in redisplay.xpi's
+    # resources/gempesaw/lib/main.js:
+
+    # var pageMod = require("sdk/page-mod");
+    # pageMod.PageMod({
+    #     include: "*.com",
+    #     contentScript: 'document.body.innerHTML = ' +
+    #         ' "<h1>Page matches ruleset</h1>";'
+    # });
+    $profile->add_extension('t/www/redisplay.xpi');
+
+    my $driver = Selenium::Remote::Driver->new(
+        extra_capabilities => {
+            firefox_profile => $profile
+        }
+    );
+
+    ok(defined $driver, "made a driver without dying");
+
+    # the initial automatic homepage load isn't blocking, so we need
+    # to wait until the page is loaded (when we can find elements)
+    $driver->set_implicit_wait_timeout(30000);
+    $driver->find_element("h1", "tag_name");
+    cmp_ok($driver->get_title, '=~', qr/google/i,
+           "profile loaded and preference respected!");
+
+    $driver->get("http://www.google.com");
+    cmp_ok($driver->get_text("body", "tag_name"), "=~", qr/ruleset/,
+           "custom profile with extension loaded");
+}
+
+PREFERENCES_FORMATTING: {
+    my $profile = Selenium::Remote::Driver::Firefox::Profile->new();
+    my $prefs = {
+        'string' => "howdy, there",
+        'integer' => 12345,
+        'true' => JSON::true,
+        'false' => JSON::false,
+        'string.like.integer' => '"12345"',
+    };
+
+    my %expected = map {
+        my $q = $_ eq 'string' ? '"' : '';
+        $_ => $q . $prefs->{$_} . $q
+    } keys %$prefs;
+
+    $profile->set_preference(%$prefs);
+
+    foreach (keys %$prefs) {
+        cmp_ok($profile->get_preference($_), "eq", $expected{$_},
+               "$_ preference is formatted properly");
+    }
+
+    my $encoded = $profile->_encode();
+    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);
+
+    foreach (keys %expected) {
+        cmp_ok($userjs, "=~", qr/$expected{$_}\);/,
+               "$_ preference is formatted properly after packing and unpacking");
+    }
+}
+
+CROAKING: {
+    my $profile = Selenium::Remote::Driver::Firefox::Profile->new();
+    {
+        eval {
+            $profile->add_extension('00-load.t');
+        };
+        ok($@ =~ /xpi format/i, "caught invalid extension filetype");
+    }
+
+    {
+        eval {
+            $profile->add_extension('t/www/invalid-extension.xpi');
+            my $test = $profile->_encode;
+        };
+        ok($@ =~ /install\.rdf/i, "caught invalid extension structure");
+    }
+
+    {
+        eval {
+            my $croakingDriver = Selenium::Remote::Driver->new(
+                extra_capabilities => {
+                    firefox_profile => 'clearly invalid!'
+                }
+            );
+        };
+        ok ($@ =~ /coercion.*failed/, "caught invalid extension in driver constructor");
+    }
+}
+
+# Kill our HTTP Server
+if ($^O eq 'MSWin32' && $record)
+{
+   system("taskkill /FI \"WINDOWTITLE eq TEMP_HTTP_SERVER\"");
+}
+elsif ($record)
+{
+    `ps aux | grep http-server\.pl | grep perl | awk '{print \$2}' | xargs kill`;
+}
+
+done_testing;

BIN
t/www/invalid-extension.xpi


BIN
t/www/redisplay.xpi