new API spec, first shot at implementation
[scpubgit/Test-Harness-Selenium.git] / lib / Test / Harness / Selenium.pm
1 package Test::Harness::Selenium;
2 use strictures 1;
3
4 use File::Find::Rule;
5 use Socialtext::WikiFixture::Selenese;
6 use HTML::TableExtract;
7 use IO::All;
8 use Alien::SeleniumRC;
9 use LWP::Simple;
10 use Child;
11
12 use Test::More;
13 BEGIN {
14   package Test::Builder;
15
16   use Class::Method::Modifiers;
17   use ExtUtils::MakeMaker qw(prompt);
18
19   if (!$ENV{AUTOMATED_TESTING}) {
20     around ok => sub {
21       my ($orig, $self) = (shift, shift);
22       my $res = $self->$orig(@_);
23       unless ($res) {
24         if ('y' eq prompt "Well that didn't work, did it. Bail out?", 'y') {
25           exit 255;
26         }
27       }
28       return $res;
29     };
30   }
31 }
32
33 sub new {
34   my $class = shift;
35   my %args = @_;
36   if( $ENV{SELENIUM_RC_HOST} && 
37       $ENV{SELENIUM_RC_PORT} &&
38       $ENV{SELENIUM_RC_START} ) {
39     $args{selenium_rc}{host} = $ENV{SELENIUM_RC_HOST};
40     $args{selenium_rc}{port} = $ENV{SELENIUM_RC_PORT};
41     $args{selenium_rc}{start} = $ENV{SELENIUM_RC_START};
42   }
43   my $self = \%args;
44   bless $self, $class;
45 }
46
47 sub start_selenium_server {
48   my($self) = @_;
49   if($self->{selenium_rc}{start}) {
50     $self->{selenium_rc}{xvnc_server_proc} = Child->new(sub {
51         system('ssh', $self->{selenium_rc}{host}, 'vncserver');
52       }
53     );
54     $self->{selenium_rc}{xvnc_server_proc}->start;
55     $self->{selenium_rc}{selenium_server_proc} = Child->new(sub {
56         system('ssh', $self->{selenium_rc}{host}, 'env',
57           "DISPLAY=:$self->{selenium_rc}{xvnc_display}", 'selenium-rc', '-port',
58           $self->{selenium_rc}{port} );
59       }
60     );
61     $self->{selenium_rc}{selenium_server_proc}->start;
62   }
63   my $tries = 0;
64   while($tries < 5) {
65     eval {
66       $self->{src} = Socialtext::WikiFixture::Selenese->new(
67         host => $self->{selenium_rc}{host},
68         port => $self->{selenium_rc}{port},
69         browser => $self->{browser},
70         browser_url => $self->{app_base},
71       );
72     };
73     $tries++;
74     if(!defined $self->{src}) {
75       sleep 10;
76     }
77     else {
78       last;
79     }
80   }
81   die "timed out waiting for selenium server to start" if $tries == 5;
82 }
83
84 sub stop_selenium_server {
85   my($self) = @_;
86   # okay, we're done, kill the server.
87   my $url = sprintf
88     "http://%s:%s/selenium-server/driver/?cmd=shutDownSeleniumServer",
89     $self->{selenium_rc}{host}, $self->{selenium_rc}{port};
90   get($url);
91   delete $self->{src};
92   $self->{selenium_rc}{selenium_server_proc}->wait;
93   $self->{selenium_rc}{xvnc_server_proc}->wait;
94 }
95
96 sub start_app_server {
97   my($self) = @_;
98   $self->{app_server_proc} = Child->new(sub { exec($self->{app_server_cmd}) } );
99   $self->{app_server_proc}->start;
100 }
101
102 sub stop_app_server {
103   my($self) = @_;
104   $self->{app_server_proc}->complete || $self->{app_server_proc}->kill(9);
105 }
106
107 sub test_directory {
108   my ($self, $dir) = @_;
109   my @tests = File::Find::Rule->file()->name('*.html')->in($dir);
110   for my $test (@tests) {
111     $self->start_server;
112     $self->run_tests_for($test);
113     $self->stop_server;
114   }
115 }
116
117 sub run_tests_for {
118   my ($self, $html_file) = @_;
119   my $rows = $self->get_rows_for($html_file);
120   $self->{src}->run_test_table($rows);
121 }
122
123 my $te = HTML::TableExtract->new;
124 sub get_rows_for {
125   my ($self, $html_file) = @_;
126   my $html = io($html_file)->all;
127   $te->parse($html);
128   my $table = ($te->tables)[0];
129   my @rows = map {
130     [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
131   } grep { defined $_->[1] } $table->rows;
132   return \@rows;
133 }
134
135 sub DESTROY {
136   my($self) = @_;
137   if(exists $self->{xvnc_pid}) {
138     kill("KILL", $self->{xvnc_pid});
139   }
140 }
141
142 1;
143
144 __END__
145
146 =head1 NAME
147
148 Test::Harness::Selenium - Test your app with Selenium
149
150 =head1 SYNOPSIS
151
152   # t/selenium.t
153   my $ths = Test::Harness::Selenium->new(
154     selenium_rc => {
155       host => 'selenium_xvnc_server',
156       port => 12345,
157       start => 1, # start xvnc and selenium RC via ssh(1)
158     },
159     browser => '*firefox',
160     # app_base is relative from the machine running selenium_rc
161     app_base => 'http://10.0.0.5:3000/',
162     app_start_cmd => 'script/myapp_server.pl -p 3000',
163   );
164   # HTML tables as emitted by the Selenium IDE
165   $ths->test_directory('t/selenium_corpus');
166
167
168   # or, if you've got a selenium server already running (say, on a designer's
169   # Win32 box)
170   my $ths = Test::Harness::Selenium->new(
171     selenium_rc=> {
172       host => 'designers_machine',
173       port => 54321,
174       start => 0, # they've already got the RC running
175     },
176     browser => '*iexplore', # can't live with it, can't live without it
177     app_base => 'http://10.0.0.5:3000/',
178     app_start_cmd => 'script/myapp_server.pl -p 3000',
179   );
180   # otherwise the same
181   $ths->test_directory('t/selenium_corpus');
182
183 =head1 DESCRIPTION
184
185 C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing
186 using the Selenium framework. It will connect to a running Selenium RC server,
187 or start one using ssh(1) to connect to a machine and then launching the RC
188 server and an xvnc(1) instance in which to run the given browser. After the
189 connection is established, Test::Harness::Selenium will read the specified HTML
190 files from disk and massage them into a format that
191 L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC
192 server. The RC server will then script the browser to do the actions described
193 in the HTML files. These actions return results, which Test::Harness::Selenium
194 then interprets as passing or failing as appropriate.
195
196 =head1 METHODS
197
198 =head2 new
199
200 =over 4
201
202 =item arguments: %attrs
203
204 =item Return value: new Test::Harness::Selenium object
205
206 =back
207
208 Constructor. Accepts a list of key/value pairs according to the following:
209
210 =over 4
211
212 =item selenium_rc
213
214 Hashref. Accepts the keys host, port, start. host and port describe the server
215 on which to start/find the Selenium RC server and possibly the xvnc server.
216 start is a Boolean indicating whether to start the Selenium RC server and xvnc
217 server.
218
219 =item browser
220
221 Scalar. The browser to use for testing the app. Must be in a form that Selenium
222 RC understands (e.g. '*firefox'); see the Selenium docs for more info.
223
224 =item app_base
225
226 Scalar. The URL, relative to the machine running Selenium RC, for the base of
227 the app. All requests made to the app are relative to this URL.
228
229 =item app_start_cmd
230
231 Scalar. This command will be run by start_app_server to run the app server to
232 test.
233
234 =back
235
236 =head2 test_directory
237
238 =item arguments: $dir
239
240 =item Return value: None
241
242 Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >>
243 files in the given directory, and then formats massages them into data
244 structures that L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC
245 server. test_directory will then output appropriate TAP according to whether the
246 tests and checks passed or failed, respectively.
247
248 =head2 run_tests_for
249
250 =item arguments: $html_file
251
252 =item Return value: None
253
254 run_tests_for is called by test_directory for each C<< .html >> file it finds in
255 the given directory.
256
257 =head2 start_app_server, start_app_server
258
259 =item Arguments: None
260
261 =item Return value: None
262
263 Start and stop the app server using the command given to the constructor.
264
265 =head2 start_selenium_server, start_selenium_server
266
267 =item Arguments: None
268
269 =item Return value: None
270
271 Start and stop the selenium / xvnc servers using the params given to the
272 constructor.
273
274 =head1 ENVIRONMENT
275
276 =head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START
277
278 These values override the matching values in the selenium_rc hashref passed to
279 new.
280
281 =head1 AUTHOR
282
283 Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk>
284
285 =head1 CONTRIBUTORS
286
287 No one, yet. Patches most welcome!
288
289 =head1 COPYRIGHT
290
291 Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and
292 "CONTRIBUTORS" as listed above.
293
294 =head1 LICENSE
295
296 This library is free software and may be distributed under the same terms as
297 perl itself.