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