3937119d51229dd4e0401af8eb5c84470bf47824
[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 test_directory {
144   my ($self, $dir) = @_;
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   my @tests = File::Find::Rule->file()->name('*.html')->in($dir);
152   for my $test (@tests) {
153     $self->run_tests_for($test);
154   }
155 }
156
157 sub run_tests_for {
158   my ($self, $html_file) = @_;
159   my $rows = $self->get_rows_for($html_file);
160   $self->{src}->run_test_table($rows);
161 }
162
163 my $te = HTML::TableExtract->new;
164 sub get_rows_for {
165   my ($self, $html_file) = @_;
166   my $html = io($html_file)->all;
167   $te->parse($html);
168   my $table = ($te->tables)[0];
169   my @rows = map {
170     [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
171   } grep { defined $_->[1] } $table->rows;
172   return \@rows;
173 }
174
175 sub done {
176   my($self) = @_;
177   $self->stop_selenium_server;
178   $self->stop_app_server;
179 }
180
181 sub DESTROY { shift->done }
182
183 1;
184
185 __END__
186
187 =head1 NAME
188
189 Test::Harness::Selenium - Test your app with Selenium
190
191 =head1 SYNOPSIS
192
193   # t/selenium.t
194   my $ths = Test::Harness::Selenium->new(
195     selenium_rc => {
196       host => 'selenium_xvnc_server',
197       port => 12345,
198       start => 1, # start xvnc and selenium RC via ssh(1)
199     },
200     browser => '*firefox',
201     # app_base is relative from the machine running selenium_rc
202     app_base => 'http://10.0.0.5:3000/',
203     app_start_cmd => 'script/myapp_server.pl -p 3000',
204   );
205   # HTML tables as emitted by the Selenium IDE
206   $ths->test_directory('t/selenium_corpus');
207
208
209   # or, if you've got a selenium server already running (say, on a designer's
210   # Win32 box)
211   my $ths = Test::Harness::Selenium->new(
212     selenium_rc=> {
213       host => 'designers_machine',
214       port => 54321,
215       start => 0, # they've already got the RC running
216     },
217     browser => '*iexplore', # can't live with it, can't live without it
218     app_base => 'http://10.0.0.5:3000/',
219     app_start_cmd => 'script/myapp_server.pl -p 3000',
220   );
221   # otherwise the same
222   $ths->test_directory('t/selenium_corpus');
223
224 =head1 DESCRIPTION
225
226 C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing
227 using the Selenium framework. It will connect to a running Selenium RC server,
228 or start one using ssh(1) to connect to a machine and then launching the RC
229 server and an xvnc(1) instance in which to run the given browser. After the
230 connection is established, Test::Harness::Selenium will read the specified HTML
231 files from disk and massage them into a format that
232 L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC
233 server. The RC server will then script the browser to do the actions described
234 in the HTML files. These actions return results, which Test::Harness::Selenium
235 then interprets as passing or failing as appropriate.
236
237 =head1 METHODS
238
239 =head2 new
240
241 =over 4
242
243 =item arguments: %attrs
244
245 =item Return value: new Test::Harness::Selenium object
246
247 =back
248
249 Constructor. Accepts a list of key/value pairs according to the following:
250
251 =over 4
252
253 =item selenium_rc
254
255 Hashref. Accepts the keys host, port, start. host and port describe the server
256 on which to start/find the Selenium RC server and possibly the xvnc server.
257 start is a Boolean indicating whether to start the Selenium RC server and xvnc
258 server.
259
260 =item browser
261
262 Scalar. The browser to use for testing the app. Must be in a form that Selenium
263 RC understands (e.g. '*firefox'); see the Selenium docs for more info.
264
265 =item app_base
266
267 Scalar. The URL, relative to the machine running Selenium RC, for the base of
268 the app. All requests made to the app are relative to this URL.
269
270 =item app_start_cmd
271
272 Scalar. This command will be run by start_app_server to run the app server to
273 test.
274
275 =back
276
277 =head2 test_directory
278
279 =item arguments: $dir
280
281 =item Return value: None
282
283 Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >>
284 files in the given directory, and then formats massages them into data
285 structures that L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC
286 server. test_directory will then output appropriate TAP according to whether the
287 tests and checks passed or failed, respectively.
288
289 =head2 run_tests_for
290
291 =item arguments: $html_file
292
293 =item Return value: None
294
295 run_tests_for is called by test_directory for each C<< .html >> file it finds in
296 the given directory.
297
298 =head2 start_app_server, start_app_server
299
300 =item Arguments: None
301
302 =item Return value: None
303
304 Start and stop the app server using the command given to the constructor.
305
306 =head2 start_selenium_server, start_selenium_server
307
308 =item Arguments: None
309
310 =item Return value: None
311
312 Start and stop the selenium / xvnc servers using the params given to the
313 constructor.
314
315 =head1 ENVIRONMENT
316
317 =head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START
318
319 These values override the matching values in the selenium_rc hashref passed to
320 new.
321
322 =head1 AUTHOR
323
324 Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk>
325
326 =head1 CONTRIBUTORS
327
328 No one, yet. Patches most welcome!
329
330 =head1 COPYRIGHT
331
332 Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and
333 "CONTRIBUTORS" as listed above.
334
335 =head1 LICENSE
336
337 This library is free software and may be distributed under the same terms as
338 perl itself.