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