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