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