File Coverage

blib/lib/Pinwheel/TestHelper.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Pinwheel::TestHelper;
2              
3 1     1   22635 use strict;
  1         3  
  1         37  
4 1     1   5 use warnings;
  1         1  
  1         31  
5              
6 1     1   5 use Cwd qw(getcwd);
  1         1  
  1         56  
7 1     1   4 use Exporter;
  1         2  
  1         45  
8 1     1   5 use Test::More;
  1         2  
  1         12  
9              
10 1     1   916 use Pinwheel::Controller qw(url_for);
  1         3  
  1         88  
11 1     1   7 use Pinwheel::Model::Date;
  1         2  
  1         23  
12 1     1   5 use Pinwheel::Model::Time;
  1         3  
  1         32  
13 1     1   622 use Pinwheel::TagSelect;
  0            
  0            
14              
15             =head1 NAME
16              
17             Pinwheel::TestHelper
18              
19             =head1 SYNOPSIS
20              
21             use Pinwheel::TestHelper;
22              
23             get('/radio4/schedule');
24             is(get_template_name(), 'schedule/day.tmpl');
25              
26             =head1 DESCRIPTION
27              
28             This is a replacement for the Pinwheel::Controller module, it is invoked in tests and overrides
29             the Pinwheel::Controller. See L.
30              
31             =head1 ROUTINES
32              
33             =over 4
34              
35             =cut
36              
37             our @ISA = qw(Exporter);
38             our @EXPORT = qw(
39             is_response
40             is_template
41             is_redirected_to
42             is_content
43             is_generated
44             is_recognised
45             is_route
46             set_time
47             set_format
48             get_template_name
49             get
50             find_nodes
51             content
52             url_for
53             localise_test_state
54             );
55              
56             our ($template, $headers, $content, $tagselect, $time_now);
57              
58             sub localise_test_state(&)
59             {
60             local $template = $template;
61             local $headers = $headers;
62             local $content = $content;
63             local $tagselect = $tagselect;
64             local $time_now = $time_now;
65             my $sub = shift; &$sub;
66             }
67              
68             =item ($headers, $content) = get($path)
69              
70             Invokes C to fetch the given page, and returns the headers
71             and content.
72              
73             Updates the C<$template, $headers, $content> variables. Most of the rest of
74             the routines in this module then examine those variables, so generally you'll
75             want to always call C first.
76              
77             =cut
78              
79             sub get
80             {
81             my ($path) = @_;
82             my ($query, $request, $page);
83              
84             ($path, $query) = split(/\?/, $path, 2);
85              
86             $template = undef;
87             $tagselect = undef;
88             $request = {
89             method => 'GET',
90             host => '127.0.0.1',
91             path => $path,
92             query => $query || '',
93             base => '',
94             time => $time_now ? $time_now->timestamp : undef,
95             };
96             ($headers, $page) = Pinwheel::Controller::dispatch($request);
97             Pinwheel::Context::get('render')->{format} = ['html'];
98             $headers = {map { @$_ } values(%$headers)};
99             $content = $page;
100             return ($headers, $page);
101             }
102              
103             sub find_nodes
104             {
105             my ($selector) = (shift);
106             my ($nodes);
107              
108             _initialise_tagselect();
109             return $tagselect->select($selector, \@_);
110             }
111              
112             =item $strings = content($selector, @selector_args)
113              
114             Selects nodes from C<$content> (so you probably want to call C first), and
115             returns an array ref of their Cs.
116              
117             See also C.
118              
119             =cut
120              
121             sub content
122             {
123             my ($selector) = (shift);
124             my ($nodes);
125              
126             _initialise_tagselect();
127             $nodes = $tagselect->select($selector, \@_);
128             return [map { $_->string_value } $nodes->get_nodelist];
129             }
130              
131             =item is_response($expect[, $name])
132              
133             Tests the 'Status' header (from C<$headers>, so you'll probably want to call
134             C first) against C<$expect>, and runs one test (via L). The
135             C<$name> is passed in as the test name.
136              
137             Allowed values for C<$expect>:
138              
139             'success' (an alias for '200')
140             'redirect' (tests that the status matches /^3\d\d$/)
141             'missing' (an alias for '404')
142             'error' (tests that the status matches /^5\d\d$/)
143             otherwise: exact match (e.g. '406')
144              
145             =cut
146              
147             sub is_response
148             {
149             my ($expect, $name) = @_;
150             my $test = _get_test_builder();
151             my $n = $headers->{'Status'};
152              
153             return $test->is_num($n, 200, $name) if ($expect eq 'success');
154             return $test->like($n, qr/^3\d\d$/, $name) if ($expect eq 'redirect');
155             return $test->is_num($n, 404, $name) if ($expect eq 'missing');
156             return $test->like($n, qr/^5\d\d$/, $name) if ($expect eq 'error');
157             return $test->is_num($n, $expect, $name);
158             }
159              
160             =item is_template($expect[, $name])
161              
162             Tests that C<$template> (as updated by C) equals C<$expect>. Runs one
163             L test, using C<$name> if supplied.
164              
165             =cut
166              
167             sub is_template
168             {
169             my ($expect, $name) = @_;
170             my $test = _get_test_builder();
171             $test->is_eq($template, $expect, $name);
172             }
173              
174             =item is_redirected_to
175              
176             C checks the "Location" header:
177              
178             # Absolute URL
179             is_redirected_to("http://....");
180              
181             # Anything else containing a slash is prefixed by "http://127.0.0.1"
182             is_redirected_to("/some/url"); # anything else containing a
183              
184             # Anything else calls url_for with only_path=0
185             is_redirected_to('some_params', for => 'url_for')
186              
187             C also checks that the "Status" header is some 3xx value.
188             If you want more fine-grained checking than that, use C.
189              
190             =cut
191              
192             sub is_redirected_to
193             {
194             my $test = _get_test_builder();
195             my $location = $headers->{'Location'};
196             my $url;
197              
198             if (scalar(@_) == 1 && $_[0] =~ /\//) {
199             $url = shift;
200             $url = "http://127.0.0.1$url" if ($url !~ /^\w+:\/\//);
201             } else {
202             $url = url_for(@_, only_path => 0);
203             }
204              
205             # Because of the way that is_redirected_to is called, it's not easy to
206             # just add an optional $name on the end. Rather than not doing names at
207             # all, we always generate a name.
208             my $name = "is_redirected_to $url";
209              
210             if ($location ne $url)
211             {
212             $test->is_eq($location, $url, $name);
213             } else {
214             is_response('redirect', $name);
215             }
216             }
217              
218             =item is_content
219              
220             is_content($selector, @selector_args, $text)
221             # or
222             is_content($selector, @selector_args, %opts)
223              
224             Finds nodes matching C<$selector, @selector_args>, then tests those nodes
225             against C<%opts>.
226              
227             The first form is equivalent to the second where C<%opts = (text =E $text)>.
228              
229             Effectively there are two ways of using C: text matching, or node counting.
230             Text matching does an implicit node count first, as it happens. The text is
231             matched against the nodes' Cs.
232              
233             # Check that exactly one item is selected, and that its string value is "this text"
234             is_content($selector, @selector_args, "this text")
235              
236             # Check that exactly two items are selected, and their string values (in order) are "One" and "Two"
237             is_content($selector, @selector_args, ["One", "Two"])
238              
239             # Check that exactly two items are selected, and the first node's string
240             # value matches the given regex, and the second node's string value is
241             # "Exact".
242             is_content($selector, @selector_args, [qr/first.pattern/, "Exact"])
243              
244             # Check that at least one item is selected
245             is_content($selector, @selector_args)
246              
247             # Check that at least 2 items are selected
248             is_content($selector, @selector_args, minimum => 2)
249              
250             # Check that at least 1 and at most 7 items are selected
251             is_content($selector, @selector_args, minimum => 1, maximum => 7)
252              
253             # Check that exactly 5 items are selected
254             is_content($selector, @selector_args, count => 5)
255              
256             The C option can be <\@text> or C<$text>. The latter case is
257             equivalent to C<[$text]>. In either case, a C option is implied, with
258             its value as the number of items in C<@text>.
259              
260             If no C<%opts> are given, C 1> is assumed.
261              
262             Tests are then run in the following order. The first failed test, if any,
263             'wins':
264              
265             =over 4
266              
267             =item count
268              
269             Tests the number of found nodes against C (exact match).
270              
271             =item minimum
272              
273             Tests the number of found nodes against C.
274              
275             =item maximum
276              
277             Tests the number of found nodes against C.
278              
279             =item text
280              
281             (If we get this far, we know that there are the same number of nodes as
282             C items).
283              
284             Each found node's C is tested against its corresponding C
285             item. Each text item can be either a plain string or a Regexp.
286              
287             =back
288              
289             =cut
290              
291             sub is_content
292             {
293             my ($selector) = shift;
294             my ($test, $nodes, %opts, $textfn, $t);
295              
296             _initialise_tagselect();
297             $test = _get_test_builder();
298             $nodes = $tagselect->select($selector, \@_);
299              
300             # Because of the way that is_content is called, it's not easy to
301             # just add an optional $name on the end. Rather than not doing names at
302             # all, we always generate a name.
303             my $name = "is_content $selector";
304              
305             if (scalar(@_) == 1) {
306             $opts{text} = shift;
307             } else {
308             %opts = @_;
309             $name = delete $opts{name}
310             if defined $opts{name};
311             }
312             if (exists($opts{text})) {
313             $t = $opts{text};
314             if (ref($t) eq 'ARRAY') {
315             $opts{count} = scalar(@$t);
316             $textfn = sub { shift @{$opts{text}} };
317             } else {
318             $opts{count} = 1;
319             $textfn = sub { $opts{text} };
320             }
321             } elsif (scalar(keys(%opts)) == 0) {
322             $opts{minimum} = 1;
323             }
324              
325             if (exists($opts{count}) && $nodes->size != $opts{count}) {
326             $test->ok(0, $name);
327             $test->diag(
328             ' found ' . $nodes->size . ' nodes,' .
329             ' expected ' . $opts{count}
330             );
331             } elsif (exists($opts{minimum}) && $nodes->size < $opts{minimum}) {
332             $test->ok(0, $name);
333             $test->diag(
334             ' found ' . $nodes->size . ' nodes,' .
335             ' expected at least ' . $opts{minimum}
336             );
337             } elsif (exists($opts{maximum}) && $nodes->size > $opts{maximum}) {
338             $test->ok(0, $name);
339             $test->diag(
340             ' found ' . $nodes->size . ' nodes,' .
341             ' expected at most ' . $opts{maximum}
342             );
343             } elsif ($textfn) {
344             foreach ($nodes->get_nodelist) {
345             $t = &$textfn();
346             if (ref($t) eq 'Regexp') {
347             return $test->like($_->string_value, $t, $name)
348             unless ($_->string_value =~ /$t/);
349             } elsif ($_->string_value ne $t) {
350             return $test->is_eq($_->string_value, $t, $name);
351             }
352             }
353             $test->ok(1, $name);
354             } else {
355             $test->ok(1, $name);
356             }
357             }
358              
359             =item is_generated
360              
361             TODO, document me.
362              
363             =cut
364              
365             sub is_generated
366             {
367             my ($path, $opts, $name) = @_;
368             my $test = _get_test_builder();
369             $test->is_eq($Pinwheel::Controller::map->generate(%$opts), $path, $name);
370             }
371              
372             =item is_recognised
373              
374             TODO, document me.
375              
376             =cut
377              
378             sub is_recognised
379             {
380             my ($opts, $path, $name) = @_;
381             my $test = _get_test_builder();
382             _is_recognised($test, $path, $opts, $name);
383             }
384              
385             =item is_route
386              
387             TODO, document me.
388              
389             =cut
390              
391             sub is_route
392             {
393             my ($path, $opts, $name) = @_;
394             my ($test, $x);
395              
396             $test = _get_test_builder();
397             $x = $Pinwheel::Controller::map->generate(%$opts);
398             return $test->is_eq($x, $path, $name) if ($x ne $path);
399             _is_recognised($test, $path, $opts, $name);
400             }
401              
402             # Not exported
403              
404             sub _is_recognised
405             {
406             my ($test, $path, $opts, $name) = @_;
407             my ($x, $v1, $v2);
408              
409             local $Test::Builder::Level = 2;
410             $x = $Pinwheel::Controller::map->match($path);
411             foreach (keys(%$opts)) {
412             if (!exists($x->{$_})) {
413             $test->ok(0, $name);
414             $test->diag(" missing key '$_' in match params");
415             return;
416             }
417             $v1 = $x->{$_};
418             $v2 = $opts->{$_};
419             if ($v1 ne $v2) {
420             $test->ok(0, $name);
421             $test->diag(" key '$_' differs: '$v1' vs '$v2'");
422             return;
423             }
424             }
425             $test->ok(1, $name);
426             }
427              
428             =item set_time(TIME)
429              
430             Sets "now" to the time given by TIME (a L object). Calls to
431             C and C will use TIME instead of the
432             system clock.
433              
434             All that C does is store TIME in C<$Pinwheel::TestHelper::time_now>. If you
435             prefer, you can assign directly, perhaps using "local".
436              
437             =cut
438              
439             sub set_time
440             {
441             $time_now = shift;
442             }
443              
444             =item set_format
445              
446             TODO, document me.
447              
448             =cut
449              
450             sub set_format
451             {
452             my ($format) = @_;
453             my ($ctx, $flist, $previous);
454              
455             $ctx = Pinwheel::Context::get('render');
456             $flist = $ctx->{format};
457             $flist = ['html'] if (!$flist || scalar(@$flist) == 0);
458             $previous = $flist->[-1];
459             $flist->[-1] = $format;
460             $ctx->{format} = $flist;
461              
462             return $previous;
463             }
464              
465             sub _get_date_now
466             {
467             my $utc = shift;
468             return real_date_now($utc) if (!defined($time_now));
469             return Pinwheel::Model::Date->new($time_now->timestamp);
470             }
471              
472             sub _get_time_now
473             {
474             my $utc = shift;
475             return real_time_now(CORE::time(), $utc) if (!defined($time_now));
476             return Pinwheel::Model::Time->new($time_now->timestamp, $utc);
477             }
478              
479             =item $template = get_template_name()
480              
481             Returns the template name that is to be used to render the page in the framework.
482              
483             =cut
484              
485             sub get_template_name
486             {
487             return $template;
488             }
489              
490              
491             sub _get_test_builder
492             {
493             return Test::More->builder;
494             }
495              
496             sub _initialise_tagselect
497             {
498             if (!$tagselect) {
499             my $s = $content;
500             $s =~ s///s;
501             $s =~ s///g;
502             $s =~ s/ / /g;
503             $s =~ s/«/«/g;
504             $s =~ s/»/»/g;
505             $s =~ s/“/“/g;
506             $s =~ s/”/”/g;
507             $tagselect = Pinwheel::TagSelect->new();
508             $tagselect->read($s);
509             }
510             }
511              
512              
513             sub test_make_template_name
514             {
515             my ($name, $ctx, $rendering);
516              
517             $name = real_make_template_name(@_);
518             $ctx = Pinwheel::Context::get('*Pinwheel::Controller');
519             $rendering = $ctx->{rendering};
520             $template = $name if ($rendering++ == 1);
521              
522             return $name;
523             }
524              
525              
526             BEGIN
527             {
528             # Trap the render functions
529             *Pinwheel::TestHelper::real_render = *Pinwheel::Controller::render;
530             *Pinwheel::TestHelper::real_make_template_name = *Pinwheel::Controller::_make_template_name;
531             *Pinwheel::Controller::_make_template_name = *Pinwheel::TestHelper::test_make_template_name;
532              
533             # Intercept Pinwheel::Model::Date::now and Pinwheel::Model::Time::now
534             *Pinwheel::TestHelper::real_date_now = *Pinwheel::Model::Date::now;
535             *Pinwheel::TestHelper::real_time_now = *Pinwheel::Model::Time::now;
536             *Pinwheel::Model::Date::now = *Pinwheel::TestHelper::_get_date_now;
537             *Pinwheel::Model::Time::now = *Pinwheel::TestHelper::_get_time_now;
538              
539             # Pull in the application components
540             require $_ foreach (glob('Config/*.pm'));
541             require $_ foreach (glob('Models/*.pm'));
542             require $_ foreach (glob('Helpers/*.pm'));
543             require $_ foreach (glob('Controllers/*.pm'));
544              
545             # Initialise the controller (and anything hooked in)
546             Pinwheel::Controller::initialise();
547              
548             # Set some defaults so url_for doesn't cause a warning without a get
549             Pinwheel::Context::set('*Pinwheel::Controller',
550             request => {
551             method => 'GET',
552             host => '127.0.0.1',
553             path => '/',
554             query => '',
555             base => '',
556             },
557             );
558             }
559              
560             =back
561              
562             =head1 AUTHOR
563              
564             A&M Network Publishing
565              
566             =cut
567              
568             1;