File Coverage

blib/lib/Mozilla/Mechanize/GUITester.pm
Criterion Covered Total %
statement 16 18 88.8
branch 1 2 50.0
condition n/a
subroutine 6 6 100.0
pod n/a
total 23 26 88.4


line stmt bran cond sub pod time code
1 11     11   618636 use strict;
  11         30  
  11         473  
2 11     11   64 use warnings FATAL => 'all';
  11         21  
  11         1030  
3              
4             package Mozilla::Mechanize::GUITester;
5              
6             BEGIN {
7             # capture possible Xlib messages on STDERR
8 11     11   12144 use IO::CaptureOutput qw(capture);
  11         426983  
  11         1128  
9             capture(sub {
10 11     11   12865 eval "use base 'Mozilla::Mechanize'";
  11         104  
  11         24  
  11         22766  
11 11     11   78 });
12 11 50       3231 die "Unable to use Mozilla::Mechanize: $@" if $@;
13             };
14              
15 11     11   8682 use Mozilla::Mechanize::GUITester::Gesture;
  0            
  0            
16             use Mozilla::PromptService;
17             use Mozilla::ObserverService;
18             use Mozilla::SourceViewer;
19             use X11::GUITest qw(ClickMouseButton :CONST SendKeys ReleaseKey
20             PressMouseButton ReleaseMouseButton PressKey
21             ResizeWindow GetScreenRes QuoteStringForSendKeys);
22             use File::Temp qw(tempdir);
23             use Mozilla::ConsoleService;
24             use Mozilla::DOM::ComputedStyle;
25             use Carp;
26             use Test::More;
27              
28             our $VERSION = '0.23';
29              
30             sub _N {
31             my ($self, $msg) = @_;
32             diag($msg . " $self->{_reqs_count}") if $ENV{MMG_NET};
33             }
34              
35             =head1 NAME
36              
37             Mozilla::Mechanize::GUITester - enhances Mozilla::Mechanize with GUI testing.
38              
39             =head1 SYNOPSIS
40              
41             use Mozilla::Mechanize::GUITester;
42              
43             # regular Mozilla::Mechanize initialization
44             my $mech = Mozilla::Mechanize::GUITester->new(%mechanize_args);
45             $mech->get_url($url);
46              
47             # convenience wrapper over GetElementById and QueryInterface
48             my $elem = $mech->get_html_element_by_id("some_id");
49              
50             # click mouse at the element position + (1, 1)
51             $mech->x_click($elem, 1, 1);
52              
53             # play with the mouse relative to the element position
54             $mech->x_mouse_down($elem, 2, 2);
55             $mech->x_mouse_move($elem, 4, 4);
56             $mech->x_mouse_up($elem, 4, 4);
57              
58             # send keystrokes to the application
59             $mech->x_send_keys('{DEL}');
60              
61             # press and release left CTRL button. You can click in the middle.
62             $mech->x_press_key('LCT');
63             $mech->x_release_key('LCT');
64              
65             # run some javascript code and print its result
66             print $mech->run_js('return "js: " + 2');
67              
68             # find out element style using its id
69             print $mech->get_element_style_by_id('the_elem_id', 'background-color');
70              
71             # are there any javascript errors?
72             print Dumper($mech->console_messages);
73              
74             # find out HTTP response status (works only for HTTP protocol)
75             print $mech->status;
76              
77             # change some text box by sending keypresses - fires all JS events
78             my $input = $mech->get_html_element_by_id("tbox", "Input");
79             $mech->x_change_text($input, "Hi");
80              
81             =head1 DESCRIPTION
82              
83             This module enhances Mozilla::Mechanize with convenience functions allowing
84             testing of DHTML/JavaScript rich pages.
85              
86             It uses X11::GUITest to emulate mouse clicking, dragging and moving over
87             elements in DOM tree.
88              
89             It also allows running of arbitrary javascript code in the page context and
90             getting back the results.
91              
92             C environment variable can be used to adjust timeout of X events
93             (given in milliseconds).
94              
95             =head1 CONSTRUCTION
96              
97             =head2 Mozilla::Mechanize::GUITester->new(%options);
98              
99             This constructor delegates to Mozilla::Mechanize::new function. See
100             Mozilla::Mechanize manual for its description.
101              
102             =cut
103             sub new {
104             my $home = $ENV{HOME};
105             my $td = tempdir("/tmp/mozilla_guitester_XXXXXX", CLEANUP => 1);
106             local $ENV{HOME} = $td;
107             local $ENV{MOZ_NO_REMOTE} = 1;
108             my $self = shift()->SUPER::new(@_);
109             $self->{_home} = $td;
110             $self->{_popups} = {};
111             $self->{_alerts} = '';
112             $self->{_console_messages} = [];
113              
114             $self->{_window_id} = $self->agent->{window}->window->XWINDOW;
115             confess("# Unable to find window id") unless $self->window_id;
116              
117             Mozilla::PromptService::Register({ DEFAULT => sub {
118             my $name = shift;
119             $self->{_popups}->{$name} = [ @_ ];
120             $self->{_alerts} .= $_[2] . "\n";
121             } , Confirm => sub { return $self->{_confirm_result} }
122             , Prompt => sub { return $self->{_prompt_result}; } });
123             $self->{_reqs_count} = 0;
124             Mozilla::ObserverService::Register({
125             'http-on-examine-response' => sub {
126             $self->_N('http-on-examine-response');
127             my $channel = shift;
128             $self->{_response_status} = $channel->responseStatus;
129             $self->{_reqs_count}-- if $self->{_reqs_count} > 0;
130             }, "http-on-modify-request" => sub {
131             $self->_N('http-on-modify-request');
132             $self->{_reqs_count}++;
133             }, "http-on-examine-cached-response" => sub {
134             $self->_N('http-on-examine-cached-response');
135             $self->{_reqs_count}-- if $self->{_reqs_count} > 0;
136             }
137             });
138             $self->{_console_handle} = Mozilla::ConsoleService::Register(sub {
139             my $msg = shift;
140             push @{ $self->console_messages }, $msg if $msg;
141             });
142             return $self;
143             }
144              
145             sub _countdown_requests {
146             my ($self) = @_;
147             $self->_N(join("", Carp::longmess()) . "_countdown_requests");
148             $self->_wait_for_gtk;
149             my $n = 1;
150             while ($self->{_reqs_count} > 0) {
151             $self->_N("iteration $n");
152             my $rq = $self->{_reqs_count};
153             $self->_wait_for_gtk;
154              
155             next if $rq != $self->{_reqs_count};
156             if (($n++ % 50) == 0) {
157             $self->_N("forcing reqs count");
158             $self->{_reqs_count}--;
159             }
160             }
161             $self->_N("_countdown_requests finish");
162             }
163              
164             sub _wait_while_busy {
165             my $self = shift;
166             $self->_countdown_requests;
167             $self->{$_} = undef for qw(forms cur_form links images);
168             return 1;
169             }
170            
171             =head1 ACCESSORS
172              
173             =head2 $mech->status
174              
175             Returns last response status using Mozilla::ObserverService and
176             nsIHTTPChannel:responseStatus function.
177              
178             Note that it works only for HTTP requests.
179              
180             =cut
181             sub status { return shift()->{_response_status}; }
182              
183             =head2 $mech->last_alert
184              
185             Returns last alert contents intercepted through Mozilla::PromptService.
186              
187             It is useful for communication from javascript.
188              
189             =cut
190             sub last_alert { return shift()->{_popups}->{Alert}->[2]; }
191              
192             =head2 $mech->console_messages
193              
194             Returns arrayref of all console messages (e.g. javascript errors) aggregated
195             so far.
196              
197             See Mozilla nsIConsoleService documentation for more details.
198              
199             =cut
200             sub console_messages { return shift()->{_console_messages}; }
201              
202             =head2 $mech->window_id
203              
204             Returns window id of guitester window.
205              
206             =cut
207             sub window_id { return shift()->{_window_id}; }
208              
209             =head1 METHODS
210              
211             =head2 $mech->x_resize_window($width, $height)
212              
213             Resizes window to $width, $height. Dies if the screen is too small for it.
214              
215             =cut
216             sub x_resize_window {
217             my ($self, $width, $height) = @_;
218             my ($x, $y) = GetScreenRes();
219             die "Screen width is too small: $x < $width" if ($x < $width);
220             die "Screen height is too small: $y < $height" if ($y < $height);
221             ResizeWindow($self->window_id, $width, $height);
222             }
223              
224             =head2 $mech->pull_alerts
225              
226             Pulls all alerts aggregated so far and resets alerts stash. Useful for JS
227             debugging.
228              
229             =cut
230             sub pull_alerts {
231             my $self = shift;
232             my $res = $self->{_alerts};
233             $self->{_alerts} = '';
234             return $res;
235             }
236              
237             =head2 $mech->set_confirm_result($res)
238              
239             Future C JavaScript calls will return C<$res> as a result.
240              
241             =cut
242             sub set_confirm_result {
243             my ($self, $res) = @_;
244             $self->{_confirm_result} = $res;
245             }
246              
247             =head2 $mech->set_prompt_result($res)
248              
249             Future prompt JavaScript calls will return C<$res> as a result.
250              
251             =cut
252             sub set_prompt_result {
253             my ($self, $res) = @_;
254             $self->{_prompt_result} = $res;
255             }
256              
257             =head2 $mech->run_js($js_code)
258              
259             Wraps $js_code with JavaScript function and invokes it. Its result is
260             returned as string and intercepted through C.
261              
262             See C accessor above.
263              
264             =cut
265             sub run_js {
266             my ($self, $js) = @_;
267             my $code = <
268             function __guitester_run_js() {
269             $js;
270             }
271             alert(__guitester_run_js());
272             ENDS
273             $self->get("javascript:$code");
274             return $self->last_alert;
275             }
276              
277             =head2 $mech->get_element_style($element, $style_attribute)
278              
279             Uses Mozilla::DOM::ComputedStyle to get property value of C<$style_attribute>
280             for the C<$element> retrieved by GetElementById previously.
281              
282             =cut
283             sub get_element_style {
284             my ($self, $el, $attr) = @_;
285             confess "No element given!" unless $el;
286             confess "No attribute given!" unless $attr;
287             return Get_Computed_Style_Property($self->get_window, $el, $attr);
288             }
289              
290             =head2 $mech->get_element_style_by_id($element_id, $style_attribute)
291              
292             Convenience function to retrieve style property by C<$element_id>. See
293             C<$mech->get_element_style>.
294              
295             =cut
296             sub get_element_style_by_id {
297             my ($self, $id, $attr) = @_;
298             return $self->get_element_style(
299             $self->get_document->GetElementById($id), $attr);
300             }
301              
302             =head2 $mech->get_full_zoom
303              
304             Returns current full zoom value
305              
306             =cut
307             sub get_full_zoom {
308             return Get_Full_Zoom(shift()->{agent}->{embed}->get_nsIWebBrowser);
309             }
310              
311             =head2 $mech->set_full_zoom($zoom)
312              
313             Sets full zoom to C<$zoom>.
314              
315             =cut
316             sub set_full_zoom {
317             return Set_Full_Zoom(shift()->{agent}->{embed}->get_nsIWebBrowser
318             , shift);
319             }
320              
321             =head2 $mech->calculated_content
322              
323             This is basically body.innerHTML content as provided by Mozilla::Mechanize.
324             See its documentation for more info.
325              
326             =cut
327             sub calculated_content {
328             return shift()->SUPER::content(@_);
329             }
330              
331             =head2 $mech->content
332              
333             This is more like "View Source" page content. It leaves html tags intact and
334             also doesn't evaluate javascript's document.write calls.
335              
336             =cut
337             sub content {
338             my $self = shift;
339             return Get_Page_Source($self->agent->{embed});
340             }
341              
342             sub gesture {
343             my ($self, $e) = @_;
344             return Mozilla::Mechanize::GUITester::Gesture->new({
345             element => $e, dom_window => $self->get_window
346             , zoom => $self->get_full_zoom
347             , window_id => $self->window_id });
348             }
349              
350             =head2 $mech->get_html_element_by_id($html_id, $elem_type)
351              
352             Uses GetElementById and QueryInterface to get Mozilla::DOM::HTMLElement.
353             If $elem_type is given queries Mozilla::DOM::HTML<$elem_type>Element.
354              
355             See Mozilla::DOM documentation for more details.
356              
357             =cut
358             sub get_html_element_by_id {
359             my ($self, $id, $type) = @_;
360             my $e = $self->get_document->GetElementById($id) or return;
361             my $dom_class = "Mozilla::DOM::HTML" . ($type || '') . "Element";
362             return $e->QueryInterface($dom_class->GetIID);
363             }
364              
365             sub _wait_for_gtk {
366             my $self = shift;
367             my $t = $ENV{MMG_TIMEOUT} || 200;
368             no warnings 'uninitialized';
369             do {
370             $self->_N("_wait_for_gtk");
371             my $run = 1;
372             Glib::Timeout->add($t, sub {
373             $self->_N("TIMEOUT");
374             $run = 0;
375             Gtk2->main_quit;
376             }, undef, -100);
377             Mozilla::DOM::ComputedStyle::Set_Poll_Timeout();
378             capture(sub { Gtk2->main while $run });
379             Mozilla::DOM::ComputedStyle::Unset_Poll_Timeout();
380             } while (Gtk2->events_pending);
381             }
382              
383             sub _with_gesture_do {
384             my ($self, $elem, $func) = @_;
385             $self->_wait_for_gtk;
386             my $g = $self->gesture($elem);
387             $func->($g);
388             $self->_wait_for_gtk;
389             }
390              
391             =head2 $mech->x_click($element, $x, $y, $times)
392              
393             Emulates mouse click at ($element.left + $x, $element.top + $y) coordinates.
394              
395             Optional C<$times> parameter can be used to specify the number of clicks sent.
396              
397             =cut
398             sub x_click {
399             my ($self, $entry, $by_left, $by_top, $num) = @_;
400             $num ||= 1;
401             $self->_with_gesture_do($entry, sub {
402             my $g = shift;
403             $g->element_mouse_move($by_left, $by_top);
404             ClickMouseButton(M_LEFT) for (1 .. $num);
405             });
406             }
407              
408             =head2 $mech->x_mouse_down($element, $x, $y)
409              
410             Presses left mouse button at ($element.left + $x, $element.top + $y).
411              
412             =cut
413             sub x_mouse_down {
414             my ($self, $entry, $by_left, $by_top) = @_;
415             $self->_with_gesture_do($entry, sub {
416             my $g = shift;
417             $g->element_mouse_move($by_left, $by_top);
418             PressMouseButton(M_LEFT);
419             });
420             }
421              
422             =head2 $mech->x_mouse_up($element, $x, $y)
423              
424             Releases left mouse button at ($element.left + $x, $element.top + $y).
425              
426             =cut
427             sub x_mouse_up {
428             my ($self, $entry, $by_left, $by_top) = @_;
429             $self->_with_gesture_do($entry, sub {
430             my $g = shift;
431             $g->element_mouse_move($by_left, $by_top);
432             ReleaseMouseButton(M_LEFT);
433             });
434             }
435              
436             =head2 $mech->x_mouse_move($element, $x, $y)
437              
438             Moves mouse to ($element.left + $x, $element.top + $y).
439              
440             =cut
441             sub x_mouse_move {
442             my ($self, $entry, $by_left, $by_top) = @_;
443             $self->_with_gesture_do($entry, sub {
444             my $g = shift;
445             $g->element_mouse_move($by_left, $by_top);
446             });
447             }
448              
449             =head2 $mech->x_send_keys($keystroke)
450              
451             Sends $keystroke to mozilla window. It uses X11::GUITest SendKeys function.
452             Please see its documentation for possible C<$keystroke> values.
453              
454             =cut
455             sub x_send_keys {
456             my ($self, $keys) = @_;
457             confess "Undefined keys" unless defined $keys;
458             SendKeys($keys) or confess "Unable to send $keys";
459             $self->_wait_for_gtk;
460             }
461              
462             =head2 $mech->x_press_key($key)
463              
464             Uses X11::GUITest PressKey function. Please see its documentation for
465             possible C<$key> values.
466              
467             =cut
468             sub x_press_key {
469             my ($self, $key) = @_;
470             PressKey($key);
471             $self->_wait_for_gtk;
472             }
473              
474             =head2 $mech->x_release_key($keystroke)
475              
476             Uses X11::GUITest ReleaseKey function to release previously pressed key.
477             Please see its X11::GUITest documentation for possible C<$key> values.
478              
479             =cut
480             sub x_release_key {
481             my ($self, $key) = @_;
482             ReleaseKey($key);
483             $self->_wait_for_gtk;
484             }
485              
486             =head2 $mech->x_change_text($input, $value)
487              
488             Changes value of C<$input> edit box to C<$value>. All JavaScript events are
489             fired. It also works on textarea element.
490              
491             =cut
492             sub x_change_text {
493             my ($self, $input, $val) = @_;
494             $input->SetValue("");
495             $self->x_click($input, 4, 4);
496             $self->x_send_keys($val ? QuoteStringForSendKeys($val) : $val);
497             $self->x_send_keys('{TAB}');
498             }
499              
500             =head2 $mech->x_change_select($input, $option_no)
501              
502             Chooses option C<$option_no> of C<$input> select. All JavaScript events are
503             fired.
504              
505             =cut
506             sub x_change_select {
507             my ($self, $input, $opno, $x, $y) = @_;
508             my $times = $opno - $input->GetSelectedIndex;
509             my $key = "{DOW}";
510             if ($times < 0) {
511             $key = "{UP}";
512             $times *= -1;
513             }
514             $self->x_click($input, $x // 7, $y // 7);
515             $self->x_send_keys($key) for (1 .. $times);
516             $self->x_send_keys('{ENT}');
517             }
518              
519             sub close {
520             my $self = shift;
521             Mozilla::ConsoleService::Unregister($self->{_console_handle});
522             $self->SUPER::close(@_);
523             }
524              
525             sub set_fields {
526             my ($self, %fields) = @_;
527             for my $n (keys %fields) {
528             my $el;
529             eval { $el = $self->get_html_element_by_id($n, "Input") };
530             next unless $el;
531             $el->GetType eq 'checkbox' or next;
532             $el->SetChecked(delete $fields{$n});
533             }
534             $self->SUPER::set_fields(%fields);
535             }
536              
537             =head2 $mech->qi($elem, $interface)
538              
539             Queries interface Mozilla::DOM::HTML$interfaceElement of C<$elem>.
540              
541             =cut
542             sub qi {
543             confess "# No element" unless $_[1];
544             my $cl = 'Mozilla::DOM::HTML' . ($_[2] || '') . "Element";
545             return $_[1]->QueryInterface($cl->GetIID);
546             }
547              
548             =head2 $mech->qi_ns($elem, $interface)
549              
550             Queries interface Mozilla::DOM::NSHTML$interfaceElement of C<$elem>.
551              
552             =cut
553             sub qi_ns {
554             confess "# No element" unless $_[1];
555             my $c = "Mozilla::DOM::NSHTML" . ($_[2] || "") . "Element";
556             return $_[1]->QueryInterface($c->GetIID);
557             }
558              
559             1;
560              
561             =head1 AUTHOR
562              
563             Boris Sukholitko
564              
565             =head1 LICENSE
566              
567             This library is free software; you can redistribute it and/or modify it under
568             the same terms as Perl itself.
569              
570             =head1 SEE ALSO
571              
572             L
573              
574             =cut
575