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