add test_recursive method for easier multi-directory testing, factor out 'start every...
[scpubgit/Test-Harness-Selenium.git] / lib / Test / Harness / Selenium.pm
CommitLineData
676409e6 1package Test::Harness::Selenium;
2use strictures 1;
3
a5dd2803 4use File::Find::Rule;
3749d2e5 5use Socialtext::WikiFixture::Selenese;
676409e6 6use HTML::TableExtract;
7use IO::All;
a5dd2803 8use Alien::SeleniumRC;
9use LWP::Simple;
69f9b61a 10use Child;
676409e6 11
525368ed 12our $VERSION = '0.01';
13
799fcc50 14use Test::More;
676409e6 15BEGIN {
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
3749d2e5 35sub new {
69f9b61a 36 my $class = shift;
37 my %args = @_;
8b643e0b 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';
69f9b61a 46 my $self = \%args;
3749d2e5 47 bless $self, $class;
48}
49
69f9b61a 50sub start_selenium_server {
a5dd2803 51 my($self) = @_;
b05fc143 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 )
a5dd2803 74 }
b05fc143 75 )->start;
76 sleep 1;
a5dd2803 77 }
69f9b61a 78 my $tries = 0;
79 while($tries < 5) {
80 eval {
1a6bcc32 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.
69f9b61a 84 $self->{src} = Socialtext::WikiFixture::Selenese->new(
1a6bcc32 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 )
69f9b61a 91 );
92 };
93 $tries++;
94 if(!defined $self->{src}) {
95 sleep 10;
96 }
97 else {
98 last;
99 }
a5dd2803 100 }
e5f319fc 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;
91053783 105 die;
e5f319fc 106 }
a5dd2803 107}
108
69f9b61a 109sub stop_selenium_server {
a5dd2803 110 my($self) = @_;
b05fc143 111 if (my $proc = delete $self->{selenium_rc}{selenium_server_proc}) {
112 my $url = sprintf
113 "http://%s:%s/selenium-server/driver/?cmd=shutDownSeleniumServer",
114 $self->{selenium_rc}{host}, $self->{selenium_rc}{port};
115 eval { get($url); }; # will fail if it never started
116 delete $self->{src};
117 $proc->kill("KILL");
118 }
119 if (delete $self->{selenium_rc}{xvnc_started}) {
120 my $host = $self->{selenium_rc}{host};
121 my @do_ssh = $host eq 'localhost' ? () : ('ssh', $host);
122 Child->new(sub {
123 exec(@do_ssh, 'vncserver', '-kill',
e5f319fc 124 ":$self->{selenium_rc}{xvnc_display}");
b05fc143 125 })->start->wait;
126 }
69f9b61a 127}
128
129sub start_app_server {
130 my($self) = @_;
5a456070 131 return unless $self->{app_server_cmd};
e5f319fc 132 my $child = Child->new(sub { exec($self->{app_server_cmd}) } );
133 $self->{app_server_proc} = $child->start;
69f9b61a 134}
135
136sub stop_app_server {
137 my($self) = @_;
5a456070 138 if (my $proc = $self->{app_server_proc}) {
139 $proc->kill("KILL");
140 }
a5dd2803 141}
142
ccb22fec 143sub start_everything {
144 my ($self) = @_;
0f44efef 145 if(!exists $self->{app_server_proc}) {
146 $self->start_app_server;
147 }
8ecad210 148 if($self->{selenium_rc}{start} && !$self->{selenium_rc}{selenium_server_proc}) {
149 $self->start_selenium_server;
150 }
ccb22fec 151}
152
153sub test_recursive {
154 my ($self, @proto) = @_;
155 my (@files, @dirs);
156 if (@proto) {
157 @files = grep /\.html$/, @proto;
158 @dirs = grep !/\.htmL$/, @proto;
159 } else {
160 $0 =~ /^(t\/.*).t$/ or die "Can't guess directory from $0";
161 @dirs = ($1);
162 }
163 $self->start_everything;
164 $self->test_file($_) for @files;
165 $self->test_directory($_) for
166 sort map File::Find::Rule->directory->in($_), @dirs;
167}
168
169sub test_directory {
170 my ($self, $dir) = @_;
171 $self->start_everything;
172 my @tests =
173 sort File::Find::Rule->file->name('*.html')->maxdepth(1)->in($dir);
174
a5dd2803 175 for my $test (@tests) {
ccb22fec 176 $self->test_file($test);
a5dd2803 177 }
676409e6 178}
179
ccb22fec 180sub test_file {
676409e6 181 my ($self, $html_file) = @_;
182 my $rows = $self->get_rows_for($html_file);
17ccdf07 183 $self->{src}->run_test_table($rows);
676409e6 184}
185
186my $te = HTML::TableExtract->new;
187sub get_rows_for {
188 my ($self, $html_file) = @_;
189 my $html = io($html_file)->all;
190 $te->parse($html);
191 my $table = ($te->tables)[0];
192 my @rows = map {
799fcc50 193 [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
194 } grep { defined $_->[1] } $table->rows;
676409e6 195 return \@rows;
196}
197
e5f319fc 198sub done {
7522d6aa 199 my($self) = @_;
5a456070 200 $self->stop_selenium_server;
8ecad210 201 $self->stop_app_server;
7522d6aa 202}
203
5a456070 204sub DESTROY { shift->done }
205
676409e6 2061;
afe57b05 207
208__END__
209
210=head1 NAME
211
212Test::Harness::Selenium - Test your app with Selenium
213
214=head1 SYNOPSIS
215
216 # t/selenium.t
217 my $ths = Test::Harness::Selenium->new(
218 selenium_rc => {
219 host => 'selenium_xvnc_server',
220 port => 12345,
221 start => 1, # start xvnc and selenium RC via ssh(1)
222 },
223 browser => '*firefox',
224 # app_base is relative from the machine running selenium_rc
225 app_base => 'http://10.0.0.5:3000/',
226 app_start_cmd => 'script/myapp_server.pl -p 3000',
227 );
228 # HTML tables as emitted by the Selenium IDE
229 $ths->test_directory('t/selenium_corpus');
230
231
232 # or, if you've got a selenium server already running (say, on a designer's
233 # Win32 box)
234 my $ths = Test::Harness::Selenium->new(
235 selenium_rc=> {
236 host => 'designers_machine',
237 port => 54321,
238 start => 0, # they've already got the RC running
239 },
240 browser => '*iexplore', # can't live with it, can't live without it
241 app_base => 'http://10.0.0.5:3000/',
242 app_start_cmd => 'script/myapp_server.pl -p 3000',
243 );
244 # otherwise the same
245 $ths->test_directory('t/selenium_corpus');
246
247=head1 DESCRIPTION
248
249C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing
250using the Selenium framework. It will connect to a running Selenium RC server,
251or start one using ssh(1) to connect to a machine and then launching the RC
252server and an xvnc(1) instance in which to run the given browser. After the
253connection is established, Test::Harness::Selenium will read the specified HTML
254files from disk and massage them into a format that
255L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC
256server. The RC server will then script the browser to do the actions described
257in the HTML files. These actions return results, which Test::Harness::Selenium
258then interprets as passing or failing as appropriate.
259
260=head1 METHODS
261
262=head2 new
263
264=over 4
265
266=item arguments: %attrs
267
268=item Return value: new Test::Harness::Selenium object
269
270=back
271
272Constructor. Accepts a list of key/value pairs according to the following:
273
274=over 4
275
276=item selenium_rc
277
278Hashref. Accepts the keys host, port, start. host and port describe the server
279on which to start/find the Selenium RC server and possibly the xvnc server.
280start is a Boolean indicating whether to start the Selenium RC server and xvnc
281server.
282
283=item browser
284
285Scalar. The browser to use for testing the app. Must be in a form that Selenium
286RC understands (e.g. '*firefox'); see the Selenium docs for more info.
287
288=item app_base
289
290Scalar. The URL, relative to the machine running Selenium RC, for the base of
291the app. All requests made to the app are relative to this URL.
292
293=item app_start_cmd
294
295Scalar. This command will be run by start_app_server to run the app server to
296test.
297
298=back
299
300=head2 test_directory
301
302=item arguments: $dir
303
304=item Return value: None
305
306Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >>
307files in the given directory, and then formats massages them into data
308structures that L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC
309server. test_directory will then output appropriate TAP according to whether the
310tests and checks passed or failed, respectively.
311
312=head2 run_tests_for
313
314=item arguments: $html_file
315
316=item Return value: None
317
318run_tests_for is called by test_directory for each C<< .html >> file it finds in
319the given directory.
320
321=head2 start_app_server, start_app_server
322
323=item Arguments: None
324
325=item Return value: None
326
327Start and stop the app server using the command given to the constructor.
328
329=head2 start_selenium_server, start_selenium_server
330
331=item Arguments: None
332
333=item Return value: None
334
335Start and stop the selenium / xvnc servers using the params given to the
336constructor.
337
338=head1 ENVIRONMENT
339
340=head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START
341
342These values override the matching values in the selenium_rc hashref passed to
343new.
344
345=head1 AUTHOR
346
347Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk>
348
349=head1 CONTRIBUTORS
350
351No one, yet. Patches most welcome!
352
353=head1 COPYRIGHT
354
355Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and
356"CONTRIBUTORS" as listed above.
357
358=head1 LICENSE
359
360This library is free software and may be distributed under the same terms as
361perl itself.