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