add guards to app server management methods to save callers needing to check if they...
[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
676409e6 143sub test_directory {
3749d2e5 144 my ($self, $dir) = @_;
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 }
3749d2e5 151 my @tests = File::Find::Rule->file()->name('*.html')->in($dir);
a5dd2803 152 for my $test (@tests) {
a5dd2803 153 $self->run_tests_for($test);
a5dd2803 154 }
676409e6 155}
156
157sub run_tests_for {
158 my ($self, $html_file) = @_;
159 my $rows = $self->get_rows_for($html_file);
17ccdf07 160 $self->{src}->run_test_table($rows);
676409e6 161}
162
163my $te = HTML::TableExtract->new;
164sub get_rows_for {
165 my ($self, $html_file) = @_;
166 my $html = io($html_file)->all;
167 $te->parse($html);
168 my $table = ($te->tables)[0];
169 my @rows = map {
799fcc50 170 [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
171 } grep { defined $_->[1] } $table->rows;
676409e6 172 return \@rows;
173}
174
e5f319fc 175sub done {
7522d6aa 176 my($self) = @_;
5a456070 177 $self->stop_selenium_server;
8ecad210 178 $self->stop_app_server;
7522d6aa 179}
180
5a456070 181sub DESTROY { shift->done }
182
676409e6 1831;
afe57b05 184
185__END__
186
187=head1 NAME
188
189Test::Harness::Selenium - Test your app with Selenium
190
191=head1 SYNOPSIS
192
193 # t/selenium.t
194 my $ths = Test::Harness::Selenium->new(
195 selenium_rc => {
196 host => 'selenium_xvnc_server',
197 port => 12345,
198 start => 1, # start xvnc and selenium RC via ssh(1)
199 },
200 browser => '*firefox',
201 # app_base is relative from the machine running selenium_rc
202 app_base => 'http://10.0.0.5:3000/',
203 app_start_cmd => 'script/myapp_server.pl -p 3000',
204 );
205 # HTML tables as emitted by the Selenium IDE
206 $ths->test_directory('t/selenium_corpus');
207
208
209 # or, if you've got a selenium server already running (say, on a designer's
210 # Win32 box)
211 my $ths = Test::Harness::Selenium->new(
212 selenium_rc=> {
213 host => 'designers_machine',
214 port => 54321,
215 start => 0, # they've already got the RC running
216 },
217 browser => '*iexplore', # can't live with it, can't live without it
218 app_base => 'http://10.0.0.5:3000/',
219 app_start_cmd => 'script/myapp_server.pl -p 3000',
220 );
221 # otherwise the same
222 $ths->test_directory('t/selenium_corpus');
223
224=head1 DESCRIPTION
225
226C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing
227using the Selenium framework. It will connect to a running Selenium RC server,
228or start one using ssh(1) to connect to a machine and then launching the RC
229server and an xvnc(1) instance in which to run the given browser. After the
230connection is established, Test::Harness::Selenium will read the specified HTML
231files from disk and massage them into a format that
232L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC
233server. The RC server will then script the browser to do the actions described
234in the HTML files. These actions return results, which Test::Harness::Selenium
235then interprets as passing or failing as appropriate.
236
237=head1 METHODS
238
239=head2 new
240
241=over 4
242
243=item arguments: %attrs
244
245=item Return value: new Test::Harness::Selenium object
246
247=back
248
249Constructor. Accepts a list of key/value pairs according to the following:
250
251=over 4
252
253=item selenium_rc
254
255Hashref. Accepts the keys host, port, start. host and port describe the server
256on which to start/find the Selenium RC server and possibly the xvnc server.
257start is a Boolean indicating whether to start the Selenium RC server and xvnc
258server.
259
260=item browser
261
262Scalar. The browser to use for testing the app. Must be in a form that Selenium
263RC understands (e.g. '*firefox'); see the Selenium docs for more info.
264
265=item app_base
266
267Scalar. The URL, relative to the machine running Selenium RC, for the base of
268the app. All requests made to the app are relative to this URL.
269
270=item app_start_cmd
271
272Scalar. This command will be run by start_app_server to run the app server to
273test.
274
275=back
276
277=head2 test_directory
278
279=item arguments: $dir
280
281=item Return value: None
282
283Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >>
284files in the given directory, and then formats massages them into data
285structures that L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC
286server. test_directory will then output appropriate TAP according to whether the
287tests and checks passed or failed, respectively.
288
289=head2 run_tests_for
290
291=item arguments: $html_file
292
293=item Return value: None
294
295run_tests_for is called by test_directory for each C<< .html >> file it finds in
296the given directory.
297
298=head2 start_app_server, start_app_server
299
300=item Arguments: None
301
302=item Return value: None
303
304Start and stop the app server using the command given to the constructor.
305
306=head2 start_selenium_server, start_selenium_server
307
308=item Arguments: None
309
310=item Return value: None
311
312Start and stop the selenium / xvnc servers using the params given to the
313constructor.
314
315=head1 ENVIRONMENT
316
317=head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START
318
319These values override the matching values in the selenium_rc hashref passed to
320new.
321
322=head1 AUTHOR
323
324Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk>
325
326=head1 CONTRIBUTORS
327
328No one, yet. Patches most welcome!
329
330=head1 COPYRIGHT
331
332Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and
333"CONTRIBUTORS" as listed above.
334
335=head1 LICENSE
336
337This library is free software and may be distributed under the same terms as
338perl itself.