use IO::All;
use Alien::SeleniumRC;
use LWP::Simple;
+use Child;
+
+our $VERSION = '0.01';
use Test::More;
BEGIN {
}
sub new {
- my ($class, $self) = @_;
- if($self->{xvnc}) {
- $ENV{DISPLAY} = $self->{xvnc};
- my $xvnc_pid = fork();
- if(!defined $xvnc_pid) {
- die "couldn't fork xvnc: $!";
+ my $class = shift;
+ my %args = @_;
+ my $selrc = ($args{selenium_rc} ||={});
+ $selrc->{$_} = $ENV{"SELENIUM_RC_${\uc $_}"}
+ for grep exists $ENV{"SELENIUM_RC_${\uc $_}"},
+ qw(host port start start_xvnc xvnc_display);
+ $selrc->{xvnc_display} ||= '0';
+ $selrc->{host} ||= 'localhost';
+ $selrc->{port} ||= 4444;
+ $args{browser} ||= '*firefox';
+ my $self = \%args;
+ bless $self, $class;
+}
+
+sub start_selenium_server {
+ my($self) = @_;
+ my $selrc = $self->{selenium_rc};
+ if($selrc->{start}) {
+ my ($host, $display, $port) = @{$selrc}{qw(host xvnc_display port)};
+ my @do_ssh = $host eq 'localhost' ? () : ('ssh', $host);
+ if ($selrc->{start_xvnc}) {
+ $selrc->{xvnc_server_proc} = Child->new(
+ sub {
+ exec(
+ @do_ssh,
+ 'vncserver', ":${display}",
+ );
+ }
+ )->start;
+ $selrc->{xvnc_started} = 1;
+ sleep 3;
}
- elsif($xvnc_pid) {
- $self->{xvnc_pid} = $xvnc_pid;
+ $selrc->{selenium_server_proc} = Child->new(
+ sub {
+ exec(
+ @do_ssh,
+ 'env', "DISPLAY=:${display}", 'selenium-rc', '-port', $port
+ )
+ }
+ )->start;
+ sleep 1;
+ }
+ my $tries = 0;
+ while($tries < 5) {
+ eval {
+ # if we don't create the ::Selenium object ourselves, then
+ # wikifixture shuts the session down after the first test table
+ # is run, at which point KABOOM when you try and run a second one.
+ $self->{src} = Socialtext::WikiFixture::Selenese->new(
+ selenium => Test::WWW::Selenium->new(
+ host => $self->{selenium_rc}{host},
+ port => $self->{selenium_rc}{port},
+ browser => $self->{browser},
+ browser_url => $self->{app_base},
+ )
+ );
+ };
+ $tries++;
+ if(!defined $self->{src}) {
+ sleep 10;
}
else {
- exec("vncserver", $self->{xvnc});
+ last;
}
}
- bless $self, $class;
+ if($tries == 5) {
+ diag "timed out waiting for selenium server to start at
+ http://$self->{selenium_rc}{host}:$self->{selenium_rc}{port}" if $tries == 5;
+ $self->done;
+ die;
+ }
}
-sub start_server {
+sub stop_selenium_server {
my($self) = @_;
- $self->{server_pid} = fork();
- if(!defined $self->{server_pid}) {
- die "can't fork: $!";
- }
- if($self->{server_pid} > 0) {
- my $tries = 0;
- while($tries < 5) {
- eval {
- $self->{src} = Socialtext::WikiFixture::Selenese->new(
- host => $self->{host},
- port => $self->{port},
- browser => $self->{browser},
- browser_url => $self->{browser_url},
- );
- };
- $tries++;
- if(!defined $self->{src}) {
- sleep 10;
- }
- else {
- last;
- }
- }
- die "timed out waiting for selenium server to start" if $tries == 5;
+ if (my $proc = delete $self->{selenium_rc}{selenium_server_proc}) {
+ my $url = sprintf
+ "http://%s:%s/selenium-server/driver/?cmd=shutDownSeleniumServer",
+ $self->{selenium_rc}{host}, $self->{selenium_rc}{port};
+ eval { get($url); }; # will fail if it never started
+ delete $self->{src};
+ $proc->kill("KILL");
}
- elsif($self->{server_pid} == 0) {
- # muttermutter, can't specify a host for selenium
- exec("$^X -MAlien::SeleniumRC -e 'Alien::SeleniumRC::start(q{-port $self->{port}})'");
+ if (delete $self->{selenium_rc}{xvnc_started}) {
+ my $host = $self->{selenium_rc}{host};
+ my @do_ssh = $host eq 'localhost' ? () : ('ssh', $host);
+ Child->new(sub {
+ exec(@do_ssh, 'vncserver', '-kill',
+ ":$self->{selenium_rc}{xvnc_display}");
+ })->start->wait;
}
}
-sub stop_server {
+sub start_app_server {
my($self) = @_;
- # okay, we're done, kill the server.
- get("http://localhost:$self->{port}/selenium-server/driver/?cmd=shutDownSeleniumServer");
- delete $self->{src};
- wait;
+ return unless $self->{app_server_cmd};
+ my $child = Child->new(sub { exec($self->{app_server_cmd}) } );
+ $self->{app_server_proc} = $child->start;
+}
+
+sub stop_app_server {
+ my($self) = @_;
+ if (my $proc = $self->{app_server_proc}) {
+ $proc->kill("KILL");
+ }
}
sub test_directory {
my ($self, $dir) = @_;
+ if(!exists $self->{app_server_proc}) {
+ $self->start_app_server;
+ }
+ if($self->{selenium_rc}{start} && !$self->{selenium_rc}{selenium_server_proc}) {
+ $self->start_selenium_server;
+ }
my @tests = File::Find::Rule->file()->name('*.html')->in($dir);
for my $test (@tests) {
- $self->start_server;
$self->run_tests_for($test);
- $self->stop_server;
}
}
return \@rows;
}
-sub DESTROY {
+sub done {
my($self) = @_;
- if(exists $self->{xvnc_pid}) {
- kill("KILL", $self->{xvnc_pid});
- }
+ $self->stop_selenium_server;
+ $self->stop_app_server;
}
+sub DESTROY { shift->done }
+
1;
+
+__END__
+
+=head1 NAME
+
+Test::Harness::Selenium - Test your app with Selenium
+
+=head1 SYNOPSIS
+
+ # t/selenium.t
+ my $ths = Test::Harness::Selenium->new(
+ selenium_rc => {
+ host => 'selenium_xvnc_server',
+ port => 12345,
+ start => 1, # start xvnc and selenium RC via ssh(1)
+ },
+ browser => '*firefox',
+ # app_base is relative from the machine running selenium_rc
+ app_base => 'http://10.0.0.5:3000/',
+ app_start_cmd => 'script/myapp_server.pl -p 3000',
+ );
+ # HTML tables as emitted by the Selenium IDE
+ $ths->test_directory('t/selenium_corpus');
+
+
+ # or, if you've got a selenium server already running (say, on a designer's
+ # Win32 box)
+ my $ths = Test::Harness::Selenium->new(
+ selenium_rc=> {
+ host => 'designers_machine',
+ port => 54321,
+ start => 0, # they've already got the RC running
+ },
+ browser => '*iexplore', # can't live with it, can't live without it
+ app_base => 'http://10.0.0.5:3000/',
+ app_start_cmd => 'script/myapp_server.pl -p 3000',
+ );
+ # otherwise the same
+ $ths->test_directory('t/selenium_corpus');
+
+=head1 DESCRIPTION
+
+C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing
+using the Selenium framework. It will connect to a running Selenium RC server,
+or start one using ssh(1) to connect to a machine and then launching the RC
+server and an xvnc(1) instance in which to run the given browser. After the
+connection is established, Test::Harness::Selenium will read the specified HTML
+files from disk and massage them into a format that
+L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC
+server. The RC server will then script the browser to do the actions described
+in the HTML files. These actions return results, which Test::Harness::Selenium
+then interprets as passing or failing as appropriate.
+
+=head1 METHODS
+
+=head2 new
+
+=over 4
+
+=item arguments: %attrs
+
+=item Return value: new Test::Harness::Selenium object
+
+=back
+
+Constructor. Accepts a list of key/value pairs according to the following:
+
+=over 4
+
+=item selenium_rc
+
+Hashref. Accepts the keys host, port, start. host and port describe the server
+on which to start/find the Selenium RC server and possibly the xvnc server.
+start is a Boolean indicating whether to start the Selenium RC server and xvnc
+server.
+
+=item browser
+
+Scalar. The browser to use for testing the app. Must be in a form that Selenium
+RC understands (e.g. '*firefox'); see the Selenium docs for more info.
+
+=item app_base
+
+Scalar. The URL, relative to the machine running Selenium RC, for the base of
+the app. All requests made to the app are relative to this URL.
+
+=item app_start_cmd
+
+Scalar. This command will be run by start_app_server to run the app server to
+test.
+
+=back
+
+=head2 test_directory
+
+=item arguments: $dir
+
+=item Return value: None
+
+Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >>
+files in the given directory, and then formats massages them into data
+structures that L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC
+server. test_directory will then output appropriate TAP according to whether the
+tests and checks passed or failed, respectively.
+
+=head2 run_tests_for
+
+=item arguments: $html_file
+
+=item Return value: None
+
+run_tests_for is called by test_directory for each C<< .html >> file it finds in
+the given directory.
+
+=head2 start_app_server, start_app_server
+
+=item Arguments: None
+
+=item Return value: None
+
+Start and stop the app server using the command given to the constructor.
+
+=head2 start_selenium_server, start_selenium_server
+
+=item Arguments: None
+
+=item Return value: None
+
+Start and stop the selenium / xvnc servers using the params given to the
+constructor.
+
+=head1 ENVIRONMENT
+
+=head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START
+
+These values override the matching values in the selenium_rc hashref passed to
+new.
+
+=head1 AUTHOR
+
+Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk>
+
+=head1 CONTRIBUTORS
+
+No one, yet. Patches most welcome!
+
+=head1 COPYRIGHT
+
+Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and
+"CONTRIBUTORS" as listed above.
+
+=head1 LICENSE
+
+This library is free software and may be distributed under the same terms as
+perl itself.