package Test::Harness::Selenium;
use strictures 1;

use File::Find::Rule;
use Socialtext::WikiFixture::Selenese;
use HTML::TableExtract;
use IO::All;
use Alien::SeleniumRC;
use LWP::Simple;

use Test::More;
BEGIN {
  package Test::Builder;

  use Class::Method::Modifiers;
  use ExtUtils::MakeMaker qw(prompt);

  if (!$ENV{AUTOMATED_TESTING}) {
    around ok => sub {
      my ($orig, $self) = (shift, shift);
      my $res = $self->$orig(@_);
      unless ($res) {
        if ('y' eq prompt "Well that didn't work, did it. Bail out?", 'y') {
          exit 255;
        }
      }
      return $res;
    };
  }
}

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: $!";
    }
    elsif($xvnc_pid) {
      $self->{xvnc_pid} = $xvnc_pid;
    }
    else {
      exec("vncserver", $self->{xvnc});
    }
  }
  bless $self, $class;
}

sub start_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;
  }
  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}})'");
  }
}

sub stop_server {
  my($self) = @_;
  # okay, we're done, kill the server.
  get("http://localhost:$self->{port}/selenium-server/driver/?cmd=shutDownSeleniumServer");
  delete $self->{src};
  wait;
}

sub test_directory {
  my ($self, $dir) = @_;
  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;
  }
}

sub run_tests_for {
  my ($self, $html_file) = @_;
  my $rows = $self->get_rows_for($html_file);
  $self->{src}->run_test_table($rows);
}

my $te = HTML::TableExtract->new;
sub get_rows_for {
  my ($self, $html_file) = @_;
  my $html = io($html_file)->all;
  $te->parse($html);
  my $table = ($te->tables)[0];
  my @rows = map {
    [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
  } grep { defined $_->[1] } $table->rows;
  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.
