Commit | Line | Data |
676409e6 |
1 | package Test::Harness::Selenium; |
2 | use strictures 1; |
3 | |
a5dd2803 |
4 | use File::Find::Rule; |
3749d2e5 |
5 | use Socialtext::WikiFixture::Selenese; |
676409e6 |
6 | use HTML::TableExtract; |
7 | use IO::All; |
a5dd2803 |
8 | use Alien::SeleniumRC; |
9 | use LWP::Simple; |
69f9b61a |
10 | use Child; |
676409e6 |
11 | |
525368ed |
12 | our $VERSION = '0.01'; |
13 | |
799fcc50 |
14 | use Test::More; |
676409e6 |
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 | |
3749d2e5 |
35 | sub 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 |
50 | sub 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 |
109 | sub 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 | |
129 | sub 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 | |
136 | sub stop_app_server { |
137 | my($self) = @_; |
5a456070 |
138 | if (my $proc = $self->{app_server_proc}) { |
139 | $proc->kill("KILL"); |
140 | } |
a5dd2803 |
141 | } |
142 | |
ccb22fec |
143 | sub test_directory { |
144 | my ($self, $dir) = @_; |
145 | $self->start_everything; |
146 | my @tests = |
147 | sort File::Find::Rule->file->name('*.html')->maxdepth(1)->in($dir); |
148 | |
a5dd2803 |
149 | for my $test (@tests) { |
ccb22fec |
150 | $self->test_file($test); |
a5dd2803 |
151 | } |
676409e6 |
152 | } |
153 | |
ccb22fec |
154 | sub test_file { |
676409e6 |
155 | my ($self, $html_file) = @_; |
156 | my $rows = $self->get_rows_for($html_file); |
17ccdf07 |
157 | $self->{src}->run_test_table($rows); |
676409e6 |
158 | } |
159 | |
84b9e9a5 |
160 | # might as well keep this object around. |
676409e6 |
161 | my $te = HTML::TableExtract->new; |
162 | sub get_rows_for { |
163 | my ($self, $html_file) = @_; |
164 | my $html = io($html_file)->all; |
165 | $te->parse($html); |
166 | my $table = ($te->tables)[0]; |
167 | my @rows = map { |
799fcc50 |
168 | [ map { (!defined $_ or $_ eq "\240") ? () : $_ } @$_ ] |
169 | } grep { defined $_->[1] } $table->rows; |
676409e6 |
170 | return \@rows; |
171 | } |
172 | |
e5f319fc |
173 | sub done { |
7522d6aa |
174 | my($self) = @_; |
5a456070 |
175 | $self->stop_selenium_server; |
8ecad210 |
176 | $self->stop_app_server; |
7522d6aa |
177 | } |
178 | |
5a456070 |
179 | sub DESTROY { shift->done } |
180 | |
676409e6 |
181 | 1; |
afe57b05 |
182 | |
183 | __END__ |
184 | |
185 | =head1 NAME |
186 | |
187 | Test::Harness::Selenium - Test your app with Selenium |
188 | |
189 | =head1 SYNOPSIS |
190 | |
84b9e9a5 |
191 | # t/catapp.t |
192 | my $browser = shift; |
193 | my $s = Test::Harness::Selenium->new( |
194 | selenium_rc => { |
195 | host => '10.0.0.8', |
196 | port => $< + 6900, |
197 | start => 1, |
198 | xvnc_display => 1, |
199 | }, |
200 | app_base => 'http://10.0.0.5:3000', |
201 | app_server_cmd => 'examples/THSelenium-Test/script/thselenium_test_server.pl', |
202 | browser => '*firefox', |
afe57b05 |
203 | ); |
204 | # HTML tables as emitted by the Selenium IDE |
84b9e9a5 |
205 | eval { $s->test_directory('t/corpus/') }; |
206 | $s->done; |
207 | done_testing; |
afe57b05 |
208 | |
209 | |
210 | # or, if you've got a selenium server already running (say, on a designer's |
211 | # Win32 box) |
212 | my $ths = Test::Harness::Selenium->new( |
213 | selenium_rc=> { |
214 | host => 'designers_machine', |
215 | port => 54321, |
216 | start => 0, # they've already got the RC running |
217 | }, |
218 | browser => '*iexplore', # can't live with it, can't live without it |
219 | app_base => 'http://10.0.0.5:3000/', |
84b9e9a5 |
220 | app_server_cmd => 'script/myapp_server.pl -p 3000', |
afe57b05 |
221 | ); |
222 | # otherwise the same |
223 | $ths->test_directory('t/selenium_corpus'); |
224 | |
225 | =head1 DESCRIPTION |
226 | |
227 | C<Test::Harness::Selenium> provides an abstracted way of doing Web app testing |
228 | using the Selenium framework. It will connect to a running Selenium RC server, |
229 | or start one using ssh(1) to connect to a machine and then launching the RC |
230 | server and an xvnc(1) instance in which to run the given browser. After the |
231 | connection is established, Test::Harness::Selenium will read the specified HTML |
232 | files from disk and massage them into a format that |
233 | L<Socialtext::WikiFixture::Selenese> can parse and send to the Selenium RC |
234 | server. The RC server will then script the browser to do the actions described |
235 | in the HTML files. These actions return results, which Test::Harness::Selenium |
236 | then interprets as passing or failing as appropriate. |
237 | |
238 | =head1 METHODS |
239 | |
240 | =head2 new |
241 | |
242 | =over 4 |
243 | |
244 | =item arguments: %attrs |
245 | |
246 | =item Return value: new Test::Harness::Selenium object |
247 | |
248 | =back |
249 | |
250 | Constructor. Accepts a list of key/value pairs according to the following: |
251 | |
252 | =over 4 |
253 | |
254 | =item selenium_rc |
255 | |
84b9e9a5 |
256 | Hashref. Accepts the keys host, port, start, start_xvnc, xvnc_display. host and |
257 | port describe the server on which to start/find the Selenium RC server and |
258 | possibly the xvnc server. start is a Boolean indicating whether to start the |
259 | Selenium RC server, whereas start_xvnc dictates the same for the xvnc server. |
260 | xvnc_display is the X11 display to point browsers at when launching them. |
afe57b05 |
261 | |
262 | =item browser |
263 | |
264 | Scalar. The browser to use for testing the app. Must be in a form that Selenium |
265 | RC understands (e.g. '*firefox'); see the Selenium docs for more info. |
266 | |
267 | =item app_base |
268 | |
269 | Scalar. The URL, relative to the machine running Selenium RC, for the base of |
270 | the app. All requests made to the app are relative to this URL. |
271 | |
84b9e9a5 |
272 | =item app_server_cmd |
afe57b05 |
273 | |
274 | Scalar. This command will be run by start_app_server to run the app server to |
275 | test. |
276 | |
277 | =back |
278 | |
279 | =head2 test_directory |
280 | |
281 | =item arguments: $dir |
282 | |
283 | =item Return value: None |
284 | |
285 | Object method. test_directory will use L<File::Find::Rule> to find all C<< .html >> |
84b9e9a5 |
286 | files in the given directory, and then massages them into data structures that |
287 | L<Socialtext::WikiFixture::Selenese> can send to the Selenium RC server. |
288 | test_directory will then output appropriate TAP according to whether the tests |
289 | and checks passed or failed, respectively. |
afe57b05 |
290 | |
84b9e9a5 |
291 | =head2 test_file |
afe57b05 |
292 | |
84b9e9a5 |
293 | =item arguments: $file |
afe57b05 |
294 | |
295 | =item Return value: None |
296 | |
84b9e9a5 |
297 | Object method. Runs the tests given in the specified file. |
afe57b05 |
298 | |
84b9e9a5 |
299 | =head2 start_app_server, stop_app_server |
afe57b05 |
300 | |
301 | =item Arguments: None |
302 | |
303 | =item Return value: None |
304 | |
84b9e9a5 |
305 | Object method. Start and stop the app server using the command given to the |
306 | constructor. References the app_server_cmd key passed to the constructor. |
afe57b05 |
307 | |
308 | =head2 start_selenium_server, start_selenium_server |
309 | |
310 | =item Arguments: None |
311 | |
312 | =item Return value: None |
313 | |
314 | Start and stop the selenium / xvnc servers using the params given to the |
315 | constructor. |
316 | |
317 | =head1 ENVIRONMENT |
318 | |
84b9e9a5 |
319 | =head2 SELENIUM_RC_HOST, SELENIUM_RC_PORT, SELENIUM_RC_START, |
320 | SELENIUM_RC_START_XVNC, SELENIUM_RC_XVNC_DISPLAY |
afe57b05 |
321 | |
322 | These values override the matching values in the selenium_rc hashref passed to |
323 | new. |
324 | |
325 | =head1 AUTHOR |
326 | |
327 | Chris Nehren <c.nehren/ths@shadowcat.co.uk>, Matt S. Trout <mst@shadowcat.co.uk> |
328 | |
329 | =head1 CONTRIBUTORS |
330 | |
84b9e9a5 |
331 | No one, yet. Patches most welcome! We most especially welcome doc patches to |
332 | make our code easier to use. We can write the best code in the world, but it |
333 | doesn't do anyone any good if it's impossible to use because of bad docs. |
afe57b05 |
334 | |
335 | =head1 COPYRIGHT |
336 | |
337 | Copyright (c) 2011 the Test::Harness::Selenium "AUTHOR" and |
338 | "CONTRIBUTORS" as listed above. |
339 | |
340 | =head1 LICENSE |
341 | |
342 | This library is free software and may be distributed under the same terms as |
343 | perl itself. |