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