new API spec, first shot at implementation
[scpubgit/Test-Harness-Selenium.git] / lib / Test / Harness / Selenium.pm
index 4766a28..fa7130b 100644 (file)
@@ -7,6 +7,7 @@ use HTML::TableExtract;
 use IO::All;
 use Alien::SeleniumRC;
 use LWP::Simple;
+use Child;
 
 use Test::More;
 BEGIN {
@@ -30,51 +31,77 @@ BEGIN {
 }
 
 sub new {
-  my ($class, $self) = @_;
+  my $class = shift;
+  my %args = @_;
+  if( $ENV{SELENIUM_RC_HOST} && 
+      $ENV{SELENIUM_RC_PORT} &&
+      $ENV{SELENIUM_RC_START} ) {
+    $args{selenium_rc}{host} = $ENV{SELENIUM_RC_HOST};
+    $args{selenium_rc}{port} = $ENV{SELENIUM_RC_PORT};
+    $args{selenium_rc}{start} = $ENV{SELENIUM_RC_START};
+  }
+  my $self = \%args;
   bless $self, $class;
 }
 
-sub start_server {
+sub start_selenium_server {
   my($self) = @_;
-  my $server_pid = fork();
-  if($server_pid > 0) {
-    $self->{server_pid} = $server_pid;
-    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;
+  if($self->{selenium_rc}{start}) {
+    $self->{selenium_rc}{xvnc_server_proc} = Child->new(sub {
+        system('ssh', $self->{selenium_rc}{host}, 'vncserver');
       }
-      else {
-        last;
+    );
+    $self->{selenium_rc}{xvnc_server_proc}->start;
+    $self->{selenium_rc}{selenium_server_proc} = Child->new(sub {
+        system('ssh', $self->{selenium_rc}{host}, 'env',
+          "DISPLAY=:$self->{selenium_rc}{xvnc_display}", 'selenium-rc', '-port',
+          $self->{selenium_rc}{port} );
       }
-    }
-    die "timed out waiting for selenium server to start" if $tries == 5;
-  }
-  elsif($server_pid == 0) {
-    close STDOUT;
-    close STDERR;
-    # muttermutter, can't specify a host for selenium
-    Alien::SeleniumRC::start("-port $self->{port}");
+    );
+    $self->{selenium_rc}{selenium_server_proc}->start;
   }
-  else {
-    die "can't fork: $!";
+  my $tries = 0;
+  while($tries < 5) {
+    eval {
+      $self->{src} = Socialtext::WikiFixture::Selenese->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 {
+      last;
+    }
   }
+  die "timed out waiting for selenium server to start" if $tries == 5;
 }
 
-sub stop_server {
+sub stop_selenium_server {
   my($self) = @_;
   # okay, we're done, kill the server.
-  get("http://localhost:$self->{port}/selenium-server/driver/?cmd=shutDownSeleniumServer");
-  wait;
+  my $url = sprintf
+    "http://%s:%s/selenium-server/driver/?cmd=shutDownSeleniumServer",
+    $self->{selenium_rc}{host}, $self->{selenium_rc}{port};
+  get($url);
+  delete $self->{src};
+  $self->{selenium_rc}{selenium_server_proc}->wait;
+  $self->{selenium_rc}{xvnc_server_proc}->wait;
+}
+
+sub start_app_server {
+  my($self) = @_;
+  $self->{app_server_proc} = Child->new(sub { exec($self->{app_server_cmd}) } );
+  $self->{app_server_proc}->start;
+}
+
+sub stop_app_server {
+  my($self) = @_;
+  $self->{app_server_proc}->complete || $self->{app_server_proc}->kill(9);
 }
 
 sub test_directory {
@@ -90,7 +117,7 @@ sub test_directory {
 sub run_tests_for {
   my ($self, $html_file) = @_;
   my $rows = $self->get_rows_for($html_file);
-  eval { $self->{src}->run_test_table($rows); };
+  $self->{src}->run_test_table($rows);
 }
 
 my $te = HTML::TableExtract->new;
@@ -105,4 +132,166 @@ sub get_rows_for {
   return \@rows;
 }
 
+sub DESTROY {
+  my($self) = @_;
+  if(exists $self->{xvnc_pid}) {
+    kill("KILL", $self->{xvnc_pid});
+  }
+}
+
 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.