kill dead code, update docs
[scpubgit/Test-Harness-Selenium.git] / lib / Test / Harness / Selenium.pm
index defe00f..8140691 100644 (file)
@@ -1,12 +1,17 @@
 package Test::Harness::Selenium;
 use strictures 1;
 
-use File::Find;
-use WWW::Selenium;
+use File::Find::Rule;
+use Socialtext::WikiFixture::Selenese;
 use HTML::TableExtract;
 use IO::All;
+use Alien::SeleniumRC;
+use LWP::Simple;
+use Child;
 
-use Test::Builder;
+our $VERSION = '0.01';
+
+use Test::More;
 BEGIN {
   package Test::Builder;
 
@@ -27,23 +32,132 @@ BEGIN {
   }
 }
 
+sub new {
+  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;
+    }
+    $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 {
+      last;
+    }
+  }
+  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 stop_selenium_server {
+  my($self) = @_;
+  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");
+  }
+  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 start_app_server {
+  my($self) = @_;
+  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) = @_;
-  my @tests = File::Find::Rule->file()->name('*.html')->in($self->{dir});
-  $self->run_tests_for($_) for @tests;
+  my ($self, $dir) = @_;
+  $self->start_everything;
+  my @tests =
+    sort File::Find::Rule->file->name('*.html')->maxdepth(1)->in($dir);
+
+  for my $test (@tests) {
+    $self->test_file($test);
+  }
 }
 
-sub run_tests_for {
+sub test_file {
   my ($self, $html_file) = @_;
   my $rows = $self->get_rows_for($html_file);
-  my $src = WWW::Selenium->new(
-    host => $args->{host},
-    port => $args->{port},
-    browser_url => $args->{browser_url},
-  );
-  $src->run_test_table($rows);
+  $self->{src}->run_test_table($rows);
 }
 
+# might as well keep this object around.
 my $te = HTML::TableExtract->new;
 sub get_rows_for {
   my ($self, $html_file) = @_;
@@ -51,9 +165,179 @@ sub get_rows_for {
   $te->parse($html);
   my $table = ($te->tables)[0];
   my @rows = map {
-    [ map { $_ eq "\240" ? () : $_ } @$_ ]
-  } $table->rows;
+    [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
+  } grep { defined $_->[1] } $table->rows;
   return \@rows;
 }
 
+sub done {
+  my($self) = @_;
+  $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/catapp.t
+  my $browser = shift;
+  my $s = Test::Harness::Selenium->new(
+      selenium_rc => {
+        host => '10.0.0.8',
+        port => $< + 6900,
+        start => 1,
+        xvnc_display => 1,
+      },
+      app_base => 'http://10.0.0.5:3000',
+      app_server_cmd => 'examples/THSelenium-Test/script/thselenium_test_server.pl',
+      browser => '*firefox',
+  );
+  # HTML tables as emitted by the Selenium IDE
+  eval { $s->test_directory('t/corpus/') };
+  $s->done;
+  done_testing;
+
+
+  # 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_server_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, start_xvnc, xvnc_display. 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, whereas start_xvnc dictates the same for the xvnc server.
+xvnc_display is the X11 display to point browsers at when launching them.
+
+=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_server_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 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 test_file
+
+=item arguments: $file
+
+=item Return value: None
+
+Object method. Runs the tests given in the specified file.
+
+=head2 start_app_server, stop_app_server
+
+=item Arguments: None
+
+=item Return value: None
+
+Object method. Start and stop the app server using the command given to the
+constructor.  References the app_server_cmd key passed 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,
+SELENIUM_RC_START_XVNC, SELENIUM_RC_XVNC_DISPLAY
+
+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! We most especially welcome doc patches to
+make our code easier to use. We can write the best code in the world, but it
+doesn't do anyone any good if it's impossible to use because of bad docs.
+
+=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.