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