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