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