Refactor server startup and shutdown
[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 {
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 }
a5dd2803 95 }
e5f319fc 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 }
a5dd2803 101}
102
69f9b61a 103sub stop_selenium_server {
a5dd2803 104 my($self) = @_;
b05fc143 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',
e5f319fc 118 ":$self->{selenium_rc}{xvnc_display}");
b05fc143 119 })->start->wait;
120 }
69f9b61a 121}
122
123sub start_app_server {
124 my($self) = @_;
e5f319fc 125 my $child = Child->new(sub { exec($self->{app_server_cmd}) } );
126 $self->{app_server_proc} = $child->start;
69f9b61a 127}
128
129sub stop_app_server {
130 my($self) = @_;
e5f319fc 131 $self->{app_server_proc}->kill("KILL");
a5dd2803 132}
133
676409e6 134sub test_directory {
3749d2e5 135 my ($self, $dir) = @_;
0f44efef 136 if(!exists $self->{app_server_proc}) {
137 $self->start_app_server;
138 }
8ecad210 139 if($self->{selenium_rc}{start} && !$self->{selenium_rc}{selenium_server_proc}) {
140 $self->start_selenium_server;
141 }
3749d2e5 142 my @tests = File::Find::Rule->file()->name('*.html')->in($dir);
a5dd2803 143 for my $test (@tests) {
a5dd2803 144 $self->run_tests_for($test);
a5dd2803 145 }
676409e6 146}
147
148sub run_tests_for {
149 my ($self, $html_file) = @_;
150 my $rows = $self->get_rows_for($html_file);
17ccdf07 151 $self->{src}->run_test_table($rows);
676409e6 152}
153
154my $te = HTML::TableExtract->new;
155sub 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 {
799fcc50 161 [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ]
162 } grep { defined $_->[1] } $table->rows;
676409e6 163 return \@rows;
164}
165
e5f319fc 166sub done {
7522d6aa 167 my($self) = @_;
fbd64e21 168 if(exists $self->{selenium_rc}{xvnc_server_proc} and
169 exists $self->{selenium_rc}{selenium_server_proc}) {
e5f319fc 170 $self->stop_selenium_server;
7522d6aa 171 }
8ecad210 172 $self->stop_app_server;
7522d6aa 173}
174
676409e6 1751;
afe57b05 176
177__END__
178
179=head1 NAME
180
181Test::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
218C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing
219using the Selenium framework. It will connect to a running Selenium RC server,
220or start one using ssh(1) to connect to a machine and then launching the RC
221server and an xvnc(1) instance in which to run the given browser. After the
222connection is established, Test::Harness::Selenium will read the specified HTML
223files from disk and massage them into a format that
224L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC
225server. The RC server will then script the browser to do the actions described
226in the HTML files. These actions return results, which Test::Harness::Selenium
227then 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
241Constructor. Accepts a list of key/value pairs according to the following:
242
243=over 4
244
245=item selenium_rc
246
247Hashref. Accepts the keys host, port, start. host and port describe the server
248on which to start/find the Selenium RC server and possibly the xvnc server.
249start is a Boolean indicating whether to start the Selenium RC server and xvnc
250server.
251
252=item browser
253
254Scalar. The browser to use for testing the app. Must be in a form that Selenium
255RC understands (e.g. '*firefox'); see the Selenium docs for more info.
256
257=item app_base
258
259Scalar. The URL, relative to the machine running Selenium RC, for the base of
260the app. All requests made to the app are relative to this URL.
261
262=item app_start_cmd
263
264Scalar. This command will be run by start_app_server to run the app server to
265test.
266
267=back
268
269=head2 test_directory
270
271=item arguments: $dir
272
273=item Return value: None
274
275Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >>
276files in the given directory, and then formats massages them into data
277structures that L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC
278server. test_directory will then output appropriate TAP according to whether the
279tests 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
287run_tests_for is called by test_directory for each C<< .html >> file it finds in
288the given directory.
289
290=head2 start_app_server, start_app_server
291
292=item Arguments: None
293
294=item Return value: None
295
296Start 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
304Start and stop the selenium / xvnc servers using the params given to the
305constructor.
306
307=head1 ENVIRONMENT
308
309=head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START
310
311These values override the matching values in the selenium_rc hashref passed to
312new.
313
314=head1 AUTHOR
315
316Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk>
317
318=head1 CONTRIBUTORS
319
320No one, yet. Patches most welcome!
321
322=head1 COPYRIGHT
323
324Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and
325"CONTRIBUTORS" as listed above.
326
327=head1 LICENSE
328
329This library is free software and may be distributed under the same terms as
330perl itself.