ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-WebDriver/WebDriver.pm
Revision: 1.12
Committed: Wed Aug 29 02:17:51 2018 UTC (5 years, 9 months ago) by root
Branch: MAIN
CVS Tags: rel-0_2
Changes since 1.11: +434 -76 lines
Log Message:
0.2

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::WebDriver - control browsers using the W3C WebDriver protocol
4    
5     =head1 SYNOPSIS
6    
7     # start geckodriver or any other w3c-compatible webdriver via the shell
8     $ geckdriver -b myfirefox/firefox --log trace --port 4444
9    
10     # then use it
11     use AnyEvent::WebDriver;
12    
13     # create a new webdriver object
14     my $wd = new AnyEvent::WebDriver;
15    
16     # create a new session with default capabilities.
17     $wd->new_session ({});
18    
19     $wd->navigate_to ("https://duckduckgo.com/html");
20     my $searchbox = $wd->find_element ("css selector" => 'input[type="text"]');
21    
22     $wd->element_send_keys ($searchbox => "free software");
23     $wd->element_click ($wd->find_element ("css selector" => 'input[type="submit"]'));
24    
25     sleep 10;
26    
27     =head1 DESCRIPTION
28    
29     This module aims to implement the W3C WebDriver specification which is the
30     standardised equivalent to the Selenium WebDriver API., which in turn aims
31     at remotely controlling web browsers such as Firefox or Chromium.
32    
33 root 1.11 At the time of this writing, it was so brand new that I ciould only get
34     C<geckodriver> (For Firefox) to work, but that is expected to be fioxed
35     very soon indeed.
36 root 1.1
37     To make most of this module, or, in fact, to make any reasonable use of
38 root 1.11 this module, you would need to refer to the W3C WebDriver recommendation,
39     which can be found L<here|https://www.w3.org/TR/webdriver1/>:
40 root 1.1
41 root 1.11 https://www.w3.org/TR/webdriver1/
42 root 1.1
43 root 1.12 =head2 CONVENTIONS
44    
45     Unless otherwise stated, all delays and time differences in this module
46     are represented as an integer number of milliseconds.
47    
48 root 1.1 =cut
49    
50     package AnyEvent::WebDriver;
51    
52     use common::sense;
53    
54     use Carp ();
55     use JSON::XS ();
56 root 1.10 use AnyEvent ();
57 root 1.1 use AnyEvent::HTTP ();
58    
59 root 1.12 our $VERSION = 0.2;
60 root 1.1
61     our $WEB_ELEMENT_IDENTIFIER = "element-6066-11e4-a52e-4f735466cecf";
62    
63     my $json = JSON::XS->new
64 root 1.9 ->utf8;
65    
66     $json->boolean_values (0, 1)
67     if $json->can ("boolean_values");
68 root 1.1
69     sub req_ {
70 root 1.9 my ($self, $method, $ep, $body, $cb) = @_;
71 root 1.1
72 root 1.9 AnyEvent::HTTP::http_request $method => "$self->{_ep}$ep",
73 root 1.1 body => $body,
74 root 1.3 timeout => $self->{timeout},
75 root 1.1 headers => { "content-type" => "application/json; charset=utf-8", "cache-control" => "no-cache" },
76 root 1.9 ($self->{proxy} eq "default" ? () : (proxy => $self->{proxy})),
77 root 1.1 sub {
78     my ($res, $hdr) = @_;
79    
80     $res = eval { $json->decode ($res) };
81     $hdr->{Status} = 500 unless exists $res->{value};
82    
83     $cb->($hdr->{Status}, $res->{value});
84     }
85     ;
86     }
87    
88     sub get_ {
89 root 1.9 my ($self, $ep, $cb) = @_;
90 root 1.1
91 root 1.9 $self->req_ (GET => $ep, undef, $cb)
92 root 1.1 }
93    
94     sub post_ {
95 root 1.9 my ($self, $ep, $data, $cb) = @_;
96 root 1.1
97 root 1.9 $self->req_ (POST => $ep, $json->encode ($data || {}), $cb)
98 root 1.1 }
99    
100     sub delete_ {
101 root 1.9 my ($self, $ep, $cb) = @_;
102 root 1.1
103 root 1.9 $self->req_ (DELETE => $ep, "", $cb)
104 root 1.1 }
105    
106     sub AUTOLOAD {
107     our $AUTOLOAD;
108    
109     $_[0]->isa (__PACKAGE__)
110     or Carp::croak "$AUTOLOAD: no such function";
111    
112     (my $name = $AUTOLOAD) =~ s/^.*://;
113    
114     my $name_ = "$name\_";
115    
116     defined &$name_
117     or Carp::croak "AUTOLOAD: no such method";
118    
119     my $func_ = \&$name_;
120    
121     *$name = sub {
122     $func_->(@_, my $cv = AE::cv);
123     my ($status, $res) = $cv->recv;
124    
125     if ($status ne "200") {
126     my $msg;
127    
128     if (exists $res->{error}) {
129     $msg = "AyEvent::WebDriver: $res->{error}: $res->{message}";
130     $msg .= "\n$res->{stacktrace}" if length $res->{stacktrace};
131     } else {
132     $msg = "AnyEvent::WebDriver: http status $status (wrong endpoint?), caught";
133     }
134    
135     Carp::croak $msg;
136     }
137    
138     $res
139     };
140    
141     goto &$name;
142     }
143    
144 root 1.12 =head2 WEBDRIVER OBJECTS
145 root 1.1
146     =over
147    
148     =item new AnyEvent::WebDriver key => value...
149    
150 root 1.9 Create a new WebDriver object. Example for a remote WebDriver connection
151 root 1.1 (the only type supported at the moment):
152    
153     my $wd = new AnyEvent::WebDriver host => "localhost", port => 4444;
154    
155     Supported keys are:
156    
157     =over
158    
159     =item endpoint => $string
160    
161     For remote connections, the endpoint to connect to (defaults to C<http://localhost:4444>).
162    
163     =item proxy => $proxyspec
164    
165     The proxy to use (same as the C<proxy> argument used by
166     L<AnyEvent::HTTP>). The default is C<undef>, which disables proxies. To
167     use the system-provided proxy (e.g. C<http_proxy> environment variable),
168     specify a value of C<default>.
169    
170     =item autodelete => $boolean
171    
172     If true (the default), then automatically execute C<delete_session> when
173     the WebDriver object is destroyed with an active session. IF set to a
174     false value, then the session will continue to exist.
175    
176 root 1.3 =item timeout => $seconds
177    
178     The HTTP timeout, in (fractional) seconds (default: C<300>, but this will
179     likely drastically reduce). This timeout is reset on any activity, so it
180 root 1.9 is not an overall request timeout. Also, individual requests might extend
181 root 1.3 this timeout if they are known to take longer.
182    
183 root 1.1 =back
184    
185     =cut
186    
187     sub new {
188     my ($class, %kv) = @_;
189    
190     bless {
191     endpoint => "http://localhost:4444",
192     proxy => undef,
193     autodelete => 1,
194 root 1.3 timeout => 300,
195 root 1.1 %kv,
196     }, $class
197     }
198    
199     sub DESTROY {
200 root 1.9 my ($self) = @_;
201 root 1.1
202 root 1.9 $self->delete_session
203     if exists $self->{sid};
204 root 1.1 }
205    
206 root 1.12 =item $al = $wd->actions
207    
208     Creates an action list associated with this WebDriver. See L<ACTION
209     LISTS>, below, for full details.
210    
211     =cut
212    
213     sub actions {
214     AnyEvent::WebDriver::Actions->new (wd => $_[0])
215     }
216    
217 root 1.1 =back
218    
219     =head2 SIMPLIFIED API
220    
221     This section documents the simplified API, which is really just a very
222     thin wrapper around the WebDriver protocol commands. They all block (using
223     L<AnyEvent> condvars) the caller until the result is available, so must
224     not be called from an event loop callback - see L<EVENT BASED API> for an
225     alternative.
226    
227 root 1.9 The method names are pretty much taken directly from the W3C WebDriver
228 root 1.1 specification, e.g. the request documented in the "Get All Cookies"
229     section is implemented via the C<get_all_cookies> method.
230    
231 root 1.9 The order is the same as in the WebDriver draft at the time of this
232 root 1.1 writing, and only minimal massaging is done to request parameters and
233     results.
234    
235     =head3 SESSIONS
236    
237     =over
238    
239     =cut
240    
241     =item $wd->new_session ({ key => value... })
242    
243 root 1.9 Try to connect to the WebDriver and initialize a new session with a
244     "new session" command, passing the given key-value pairs as value
245 root 1.1 (e.g. C<capabilities>).
246    
247     No session-dependent methods must be called before this function returns
248 root 1.9 successfully, and only one session can be created per WebDriver object.
249 root 1.1
250 root 1.7 On success, C<< $wd->{sid} >> is set to the session ID, and C<<
251 root 1.1 $wd->{capabilities} >> is set to the returned capabilities.
252    
253 root 1.4 my $wd = new AnyEvent::Selenium endpoint => "http://localhost:4545";
254 root 1.1
255     $wd->new_session ({
256     capabilities => {
257     pageLoadStrategy => "normal",
258     }.
259     });
260    
261     =cut
262    
263     sub new_session_ {
264 root 1.9 my ($self, $kv, $cb) = @_;
265 root 1.1
266 root 1.9 local $self->{_ep} = "$self->{endpoint}/";
267     $self->post_ (session => $kv, sub {
268 root 1.1 my ($status, $res) = @_;
269    
270     if ($status eq "200") {
271 root 1.9 $self->{sid} = $res->{sessionId};
272     $self->{capabilities} = $res->{capabilities};
273 root 1.1
274 root 1.9 $self->{_ep} = "$self->{endpoint}/session/$self->{sid}/";
275 root 1.1 }
276    
277     $cb->($status, $res);
278     });
279     }
280    
281     =item $wd->delete_session
282    
283     Deletes the session - the WebDriver object must not be used after this
284     call.
285    
286     =cut
287    
288     sub delete_session_ {
289 root 1.9 my ($self, $cb) = @_;
290 root 1.1
291 root 1.9 local $self->{_ep} = "$self->{endpoint}/session/$self->{sid}";
292     $self->delete_ ("" => $cb);
293 root 1.1 }
294    
295     =item $timeouts = $wd->get_timeouts
296    
297     Get the current timeouts, e.g.:
298    
299     my $timeouts = $wd->get_timeouts;
300 root 1.5 => { implicit => 0, pageLoad => 300000, script => 30000 }
301 root 1.1
302     =item $wd->set_timeouts ($timeouts)
303    
304     Sets one or more timeouts, e.g.:
305    
306     $wd->set_timeouts ({ script => 60000 });
307    
308     =cut
309    
310     sub get_timeouts_ {
311     $_[0]->get_ (timeouts => $_[1], $_[2]);
312     }
313    
314     sub set_timeouts_ {
315     $_[0]->post_ (timeouts => $_[1], $_[2], $_[3]);
316     }
317    
318     =back
319    
320     =head3 NAVIGATION
321    
322     =over
323    
324     =cut
325    
326     =item $wd->navigate_to ($url)
327    
328     Navigates to the specified URL.
329    
330     =item $url = $wd->get_current_url
331    
332 root 1.8 Queries the current page URL as set by C<navigate_to>.
333 root 1.1
334     =cut
335    
336     sub navigate_to_ {
337     $_[0]->post_ (url => { url => "$_[1]" }, $_[2]);
338     }
339    
340     sub get_current_url_ {
341     $_[0]->get_ (url => $_[1])
342     }
343    
344     =item $wd->back
345    
346     The equivalent of pressing "back" in the browser.
347    
348     =item $wd->forward
349    
350     The equivalent of pressing "forward" in the browser.
351    
352     =item $wd->refresh
353    
354     The equivalent of pressing "refresh" in the browser.
355    
356     =cut
357    
358     sub back_ {
359     $_[0]->post_ (back => undef, $_[1]);
360     }
361    
362     sub forward_ {
363     $_[0]->post_ (forward => undef, $_[1]);
364     }
365    
366     sub refresh_ {
367     $_[0]->post_ (refresh => undef, $_[1]);
368     }
369    
370     =item $title = $wd->get_title
371    
372     Returns the current document title.
373    
374     =cut
375    
376     sub get_title_ {
377     $_[0]->get_ (title => $_[1]);
378     }
379    
380     =back
381    
382     =head3 COMMAND CONTEXTS
383    
384     =over
385    
386     =cut
387    
388     =item $handle = $wd->get_window_handle
389    
390     Returns the current window handle.
391    
392     =item $wd->close_window
393    
394     Closes the current browsing context.
395    
396     =item $wd->switch_to_window ($handle)
397    
398     Changes the current browsing context to the given window.
399    
400     =cut
401    
402     sub get_window_handle_ {
403     $_[0]->get_ (window => $_[1]);
404     }
405    
406     sub close_window_ {
407     $_[0]->delete_ (window => $_[1]);
408     }
409    
410     sub switch_to_window_ {
411     $_[0]->post_ (window => "$_[1]", $_[2]);
412     }
413    
414     =item $handles = $wd->get_window_handles
415    
416     Return the current window handles as an array-ref of handle IDs.
417    
418     =cut
419    
420     sub get_window_handles_ {
421     $_[0]->get_ ("window/handles" => $_[1]);
422     }
423    
424     =item $handles = $wd->switch_to_frame ($frame)
425    
426 root 1.8 Switch to the given frame identified by C<$frame>, which must be either
427     C<undef> to go back to the top-level browsing context, an integer to
428 root 1.12 select the nth subframe, or an element object.
429 root 1.1
430     =cut
431    
432     sub switch_to_frame_ {
433     $_[0]->post_ (frame => { id => "$_[1]" }, $_[2]);
434     }
435    
436     =item $handles = $wd->switch_to_parent_frame
437    
438     Switch to the parent frame.
439    
440     =cut
441    
442     sub switch_to_parent_frame_ {
443     $_[0]->post_ ("frame/parent" => undef, $_[1]);
444     }
445    
446     =item $rect = $wd->get_window_rect
447    
448     Return the current window rect, e.g.:
449    
450     $rect = $wd->get_window_rect
451 root 1.5 => { height => 1040, width => 540, x => 0, y => 0 }
452 root 1.1
453     =item $wd->set_window_rect ($rect)
454    
455     Sets the window rect.
456    
457     =cut
458    
459     sub get_window_rect_ {
460     $_[0]->get_ ("window/rect" => $_[1]);
461     }
462    
463     sub set_window_rect_ {
464     $_[0]->post_ ("window/rect" => $_[1], $_[2]);
465     }
466    
467     =item $wd->maximize_window
468    
469     =item $wd->minimize_window
470    
471     =item $wd->fullscreen_window
472    
473 root 1.3 Changes the window size by either maximising, minimising or making it
474 root 1.9 fullscreen. In my experience, this will timeout if no window manager is
475 root 1.1 running.
476    
477     =cut
478    
479     sub maximize_window_ {
480     $_[0]->post_ ("window/maximize" => undef, $_[1]);
481     }
482    
483     sub minimize_window_ {
484     $_[0]->post_ ("window/minimize" => undef, $_[1]);
485     }
486    
487     sub fullscreen_window_ {
488     $_[0]->post_ ("window/fullscreen" => undef, $_[1]);
489     }
490    
491     =back
492    
493     =head3 ELEMENT RETRIEVAL
494    
495     =over
496    
497     =cut
498    
499 root 1.12 =item $element = $wd->find_element ($location_strategy, $selector)
500 root 1.1
501     Finds the first element specified by the given selector and returns its
502 root 1.12 element object. Raises an error when no element was found.
503 root 1.1
504     $element = $wd->find_element ("css selector" => "body a");
505     $element = $wd->find_element ("link text" => "Click Here For Porn");
506     $element = $wd->find_element ("partial link text" => "orn");
507     $element = $wd->find_element ("tag name" => "input");
508     $element = $wd->find_element ("xpath" => '//input[@type="text"]');
509 root 1.12 => e.g. { "element-6066-11e4-a52e-4f735466cecf" => "decddca8-5986-4e1d-8c93-efe952505a5f" }
510 root 1.1
511 root 1.12 =item $elements = $wd->find_elements ($location_strategy, $selector)
512 root 1.1
513 root 1.12 As above, but returns an arrayref of all found element objects.
514 root 1.1
515 root 1.12 =item $element = $wd->find_element_from_element ($element, $location_strategy, $selector)
516 root 1.1
517     Like C<find_element>, but looks only inside the specified C<$element>.
518    
519 root 1.12 =item $elements = $wd->find_elements_from_element ($element, $location_strategy, $selector)
520 root 1.1
521     Like C<find_elements>, but looks only inside the specified C<$element>.
522    
523     my $head = $wd->find_element ("tag name" => "head");
524     my $links = $wd->find_elements_from_element ($head, "tag name", "link");
525    
526 root 1.12 =item $element = $wd->get_active_element
527 root 1.1
528     Returns the active element.
529    
530     =cut
531    
532     sub find_element_ {
533 root 1.12 $_[0]->post_ (element => { using => "$_[1]", value => "$_[2]" }, $_[3]);
534 root 1.1 }
535    
536     sub find_elements_ {
537 root 1.12 $_[0]->post_ (elements => { using => "$_[1]", value => "$_[2]" }, $_[3]);
538 root 1.1 }
539    
540     sub find_element_from_element_ {
541 root 1.12 $_[0]->post_ ("element/$_[1]/element" => { using => "$_[2]", value => "$_[3]" }, $_[4]);
542 root 1.1 }
543    
544     sub find_elements_from_element_ {
545 root 1.12 $_[0]->post_ ("element/$_[1]/elements" => { using => "$_[2]", value => "$_[3]" }, $_[4]);
546 root 1.1 }
547    
548     sub get_active_element_ {
549 root 1.12 $_[0]->get_ ("element/active" => $_[1]);
550 root 1.1 }
551    
552     =back
553    
554     =head3 ELEMENT STATE
555    
556     =over
557    
558     =cut
559    
560     =item $bool = $wd->is_element_selected
561    
562     Returns whether the given input or option element is selected or not.
563    
564 root 1.12 =item $string = $wd->get_element_attribute ($element, $name)
565 root 1.1
566     Returns the value of the given attribute.
567    
568 root 1.12 =item $string = $wd->get_element_property ($element, $name)
569 root 1.1
570     Returns the value of the given property.
571    
572 root 1.12 =item $string = $wd->get_element_css_value ($element, $name)
573 root 1.1
574 root 1.9 Returns the value of the given CSS value.
575 root 1.1
576 root 1.12 =item $string = $wd->get_element_text ($element)
577 root 1.1
578     Returns the (rendered) text content of the given element.
579    
580 root 1.12 =item $string = $wd->get_element_tag_name ($element)
581 root 1.1
582     Returns the tag of the given element.
583    
584 root 1.12 =item $rect = $wd->get_element_rect ($element)
585 root 1.1
586 root 1.9 Returns the element rect(angle) of the given element.
587 root 1.1
588     =item $bool = $wd->is_element_enabled
589    
590     Returns whether the element is enabled or not.
591    
592     =cut
593    
594     sub is_element_selected_ {
595 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/selected" => $_[2]);
596 root 1.1 }
597    
598     sub get_element_attribute_ {
599 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/attribute/$_[2]" => $_[3]);
600 root 1.1 }
601    
602     sub get_element_property_ {
603 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/property/$_[2]" => $_[3]);
604 root 1.1 }
605    
606     sub get_element_css_value_ {
607 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/css/$_[2]" => $_[3]);
608 root 1.1 }
609    
610     sub get_element_text_ {
611 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/text" => $_[2]);
612 root 1.1 }
613    
614     sub get_element_tag_name_ {
615 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/name" => $_[2]);
616 root 1.1 }
617    
618     sub get_element_rect_ {
619 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/rect" => $_[2]);
620 root 1.1 }
621    
622     sub is_element_enabled_ {
623 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/enabled" => $_[2]);
624 root 1.1 }
625    
626     =back
627    
628     =head3 ELEMENT INTERACTION
629    
630     =over
631    
632     =cut
633    
634 root 1.12 =item $wd->element_click ($element)
635 root 1.1
636     Clicks the given element.
637    
638 root 1.12 =item $wd->element_clear ($element)
639 root 1.1
640     Clear the contents of the given element.
641    
642 root 1.12 =item $wd->element_send_keys ($element, $text)
643 root 1.1
644     Sends the given text as key events to the given element.
645    
646     =cut
647    
648     sub element_click_ {
649 root 1.12 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/click" => undef, $_[2]);
650 root 1.1 }
651    
652     sub element_clear_ {
653 root 1.12 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/clear" => undef, $_[2]);
654 root 1.1 }
655    
656     sub element_send_keys_ {
657 root 1.12 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/value" => { text => "$_[2]" }, $_[3]);
658 root 1.1 }
659    
660     =back
661    
662     =head3 DOCUMENT HANDLING
663    
664     =over
665    
666     =cut
667    
668     =item $source = $wd->get_page_source
669    
670     Returns the (HTML/XML) page source of the current document.
671    
672     =item $results = $wd->execute_script ($javascript, $args)
673    
674     Synchronously execute the given script with given arguments and return its
675     results (C<$args> can be C<undef> if no arguments are wanted/needed).
676    
677     $ten = $wd->execute_script ("return arguments[0]+arguments[1]", [3, 7]);
678    
679     =item $results = $wd->execute_async_script ($javascript, $args)
680    
681     Similar to C<execute_script>, but doesn't wait for script to return, but
682     instead waits for the script to call its last argument, which is added to
683     C<$args> automatically.
684    
685     $twenty = $wd->execute_async_script ("arguments[0](20)", undef);
686    
687     =cut
688    
689     sub get_page_source_ {
690     $_[0]->get_ (source => $_[1]);
691     }
692    
693     sub execute_script_ {
694     $_[0]->post_ ("execute/sync" => { script => "$_[1]", args => $_[2] || [] }, $_[3]);
695     }
696    
697     sub execute_async_script_ {
698     $_[0]->post_ ("execute/async" => { script => "$_[1]", args => $_[2] || [] }, $_[3]);
699     }
700    
701     =back
702    
703     =head3 COOKIES
704    
705     =over
706    
707     =cut
708    
709     =item $cookies = $wd->get_all_cookies
710    
711     Returns all cookies, as an arrayref of hashrefs.
712    
713     # google surely sets a lot of cookies without my consent
714     $wd->navigate_to ("http://google.com");
715     use Data::Dump;
716     ddx $wd->get_all_cookies;
717    
718     =item $cookie = $wd->get_named_cookie ($name)
719    
720     Returns a single cookie as a hashref.
721    
722     =item $wd->add_cookie ($cookie)
723    
724     Adds the given cookie hashref.
725    
726     =item $wd->delete_cookie ($name)
727    
728     Delete the named cookie.
729    
730     =item $wd->delete_all_cookies
731    
732     Delete all cookies.
733    
734     =cut
735    
736     sub get_all_cookies_ {
737     $_[0]->get_ (cookie => $_[1]);
738     }
739    
740     sub get_named_cookie_ {
741     $_[0]->get_ ("cookie/$_[1]" => $_[2]);
742     }
743    
744     sub add_cookie_ {
745     $_[0]->post_ (cookie => { cookie => $_[1] }, $_[2]);
746     }
747    
748     sub delete_cookie_ {
749     $_[0]->delete_ ("cookie/$_[1]" => $_[2]);
750     }
751    
752     sub delete_all_cookies_ {
753     $_[0]->delete_ (cookie => $_[2]);
754     }
755    
756     =back
757    
758     =head3 ACTIONS
759    
760     =over
761    
762     =cut
763    
764     =item $wd->perform_actions ($actions)
765    
766     Perform the given actions (an arrayref of action specifications simulating
767 root 1.12 user activity, or an C<AnyEvent::WebDriver::Actions> object). For further
768     details, read the spec or the section L<ACTION LISTS>, below.
769 root 1.1
770 root 1.12 An example to get you started (see the next example for a mostly
771     equivalent example using the C<AnyEvent::WebDriver::Actions> helper API):
772 root 1.1
773     $wd->navigate_to ("https://duckduckgo.com/html");
774     my $input = $wd->find_element ("css selector", 'input[type="text"]');
775     $wd->perform_actions ([
776     {
777     id => "myfatfinger",
778     type => "pointer",
779     pointerType => "touch",
780     actions => [
781 root 1.12 { type => "pointerMove", duration => 100, origin => $input, x => 40, y => 5 },
782 root 1.1 { type => "pointerDown", button => 1 },
783     { type => "pause", duration => 40 },
784     { type => "pointerUp", button => 1 },
785     ],
786     },
787     {
788     id => "mykeyboard",
789     type => "key",
790     actions => [
791     { type => "pause" },
792     { type => "pause" },
793     { type => "pause" },
794     { type => "pause" },
795     { type => "keyDown", value => "a" },
796     { type => "pause", duration => 100 },
797     { type => "keyUp", value => "a" },
798     { type => "pause", duration => 100 },
799     { type => "keyDown", value => "b" },
800     { type => "pause", duration => 100 },
801     { type => "keyUp", value => "b" },
802     { type => "pause", duration => 2000 },
803     { type => "keyDown", value => "\x{E007}" }, # enter
804     { type => "pause", duration => 100 },
805     { type => "keyUp", value => "\x{E007}" }, # enter
806     { type => "pause", duration => 5000 },
807     ],
808     },
809     ]);
810    
811 root 1.12 And here is essentially the same (except for fewer pauses) example as
812     above, using the much simpler C<AnyEvent::WebDriver::Actions> API. Note
813     that the pointer up and key down event happen concurrently in this
814     example:
815    
816     $wd->navigate_to ("https://duckduckgo.com/html");
817     my $input = $wd->find_element ("css selector", 'input[type="text"]');
818     $wd->actions
819     ->move ($input, 40, 5, "touch1")
820     ->click;
821     ->key ("a");
822     ->key ("b");
823     ->pause (2000);
824     ->key ("\x{E007}")
825     ->pause (5000);
826     ->perform;
827    
828 root 1.1 =item $wd->release_actions
829    
830     Release all keys and pointer buttons currently depressed.
831    
832     =cut
833    
834     sub perform_actions_ {
835 root 1.12 if (UNIVERSAL::isa $_[1], AnyEvent::WebDriver::Actions::) {
836     my ($actions, $duration) = $_[1]->compile;
837     local $_[0]{timeout} = $_[0]{timeout} + $duration * 1e-3;
838     $_[0]->post_ (actions => { actions => $actions }, $_[2]);
839     } else {
840     $_[0]->post_ (actions => { actions => $_[1] }, $_[2]);
841     }
842 root 1.1 }
843    
844     sub release_actions_ {
845     $_[0]->delete_ (actions => $_[1]);
846     }
847    
848     =back
849    
850     =head3 USER PROMPTS
851    
852     =over
853    
854     =cut
855    
856     =item $wd->dismiss_alert
857    
858     Dismiss a simple dialog, if present.
859    
860     =item $wd->accept_alert
861    
862     Accept a simple dialog, if present.
863    
864     =item $text = $wd->get_alert_text
865    
866     Returns the text of any simple dialog.
867    
868     =item $text = $wd->send_alert_text
869    
870     Fills in the user prompt with the given text.
871    
872    
873     =cut
874    
875     sub dismiss_alert_ {
876     $_[0]->post_ ("alert/dismiss" => undef, $_[1]);
877     }
878    
879     sub accept_alert_ {
880     $_[0]->post_ ("alert/accept" => undef, $_[1]);
881     }
882    
883     sub get_alert_text_ {
884     $_[0]->get_ ("alert/text" => $_[1]);
885     }
886    
887     sub send_alert_text_ {
888     $_[0]->post_ ("alert/text" => { text => "$_[1]" }, $_[2]);
889     }
890    
891     =back
892    
893     =head3 SCREEN CAPTURE
894    
895     =over
896    
897     =cut
898    
899     =item $wd->take_screenshot
900    
901 root 1.9 Create a screenshot, returning it as a PNG image in a C<data:> URL.
902 root 1.1
903 root 1.12 =item $wd->take_element_screenshot ($element)
904 root 1.1
905     Accept a simple dialog, if present.
906    
907     =cut
908    
909     sub take_screenshot_ {
910     $_[0]->get_ (screenshot => $_[1]);
911     }
912    
913     sub take_element_screenshot_ {
914 root 1.12 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/screenshot" => $_[2]);
915 root 1.1 }
916    
917     =back
918    
919 root 1.12 =head2 ACTION LISTS
920    
921     Action lists can be quite complicated. Or at least it took a while for
922     me to twist my head around them. Basically, an action list consists of a
923     number of sources representing devices (such as a finger, a mouse, a pen
924     or a keyboard) and a list of actions for each source.
925    
926     An action can be a key press, a pointer move or a pause (time
927     delay). Actions from different sources can happen "at the same time",
928     while actions from a single source are executed in order.
929    
930     While you can provide an action list manually, it is (hopefully) less
931     cumbersome to use the API described in this section to create them.
932    
933     The basic process of creating and performing actions is to create a new
934     action list, adding action sources, followed by adding actions. Finally
935     you would C<perform> those actions on the WebDriver.
936    
937     Virtual time progresses as long as you add actions to the same event
938     source. Adding events to different sources are considered to happen
939     concurrently. If you want to force time to progress, you can do this using
940     a call to C<< ->pause (0) >>.
941    
942     Most methods here are designed to chain, i.e. they return the web actions
943     object, to simplify multiple calls.
944    
945     For example, to simulate a mouse click to an input element, followed by
946     entering some text and pressing enter, you can use this:
947    
948     $wd->actions
949     ->click (1, 100)
950     ->type ("some text")
951     ->key ("Enter")
952     ->perform;
953    
954     By default, keyboard and mouse input sources are provided. You can create
955     your own sources and use them when adding events. The above example could
956     be more verbosely written like this:
957    
958     $wd->actions
959     ->click (1, 100, "mouse")
960     ->type ("some text")
961     ->key ("Enter")
962     ->perform;
963    
964    
965    
966     #TODO verboser example
967    
968     When you specify the event source expliticly it will switch the current
969     "focus" for this class of device (all keyboards are in one class, all
970     pointer-like devices such as mice/fingers/pens are in one class), so you
971     don't have to specify the source for subsequent actions.
972    
973     When you use the sources C<keyboard>, C<mouse>, C<touch1>..C<touch3>,
974     C<pen> without defining them, then a suitable default source will be
975     created for them.
976    
977     =over 4
978    
979     =cut
980    
981     package AnyEvent::WebDriver::Actions;
982    
983     =item $al = new AnyEvent::WebDriver::Actions
984    
985     Create a new empty action list object. More often you would use the C<<
986     $sel->action_list >> method to create one that is already associated with
987     a given web driver.
988    
989     =cut
990    
991     sub new {
992     my ($class, %kv) = @_;
993    
994     $kv{last_kbd} = "keyboard";
995     $kv{last_ptr} = "mouse";
996    
997     bless \%kv, $class
998     }
999    
1000     =item $al = $al->source ($id, $type, key => value...)
1001    
1002     The first time you call this with a givne ID, this defines the event
1003     source using the extra parameters. Subsequent calls merely switch the
1004     current source for its event class.
1005    
1006     It's not an error to define built-in sources (such as C<keyboard> or
1007     C<touch1>) differently then the defaults.
1008    
1009     Example: define a new touch device called C<fatfinger>.
1010    
1011     $al->source (fatfinger => "pointer", pointerType => "touch");
1012    
1013     Example: switchdefine a new touch device called C<fatfinger>.
1014    
1015     $al->source (fatfinger => "pointer", pointerType => "touch");
1016    
1017     =cut
1018    
1019     sub _default_source {
1020     my ($source) = @_;
1021    
1022     $source eq "keyboard" ? { actions => [], id => $source, type => "key" }
1023     : $source eq "mouse" ? { actions => [], id => $source, type => "pointer", pointerType => "mouse" }
1024     : $source eq "touch" ? { actions => [], id => $source, type => "pointer", pointerType => "touch" }
1025     : $source eq "pen" ? { actions => [], id => $source, type => "pointer", pointerType => "pen" }
1026     : Carp::croak "AnyEvent::WebDriver::Actions: event source '$source' not defined"
1027     }
1028    
1029     my %source_class = (
1030     key => "kbd",
1031     pointer => "ptr",
1032     );
1033    
1034     sub source {
1035     my ($self, $id, $type, %kv) = @_;
1036    
1037     if (defined $type) {
1038     !exists $self->{source}{$id}
1039     or Carp::croak "AnyEvent::WebDriver::Actions: source '$id' already defined";
1040    
1041     $kv{id} = $id;
1042     $kv{type} = $type;
1043     $kv{actions} = [];
1044    
1045     $self->{source}{$id} = \%kv;
1046     }
1047    
1048     my $source = $self->{source}{$id} ||= _default_source $id;
1049    
1050     my $last = $source_class{$source->{type}} // "xxx";
1051    
1052     $self->{"last_$last"} = $id;
1053    
1054     $self
1055     }
1056    
1057     sub _add {
1058     my ($self, $source, $sourcetype, $type, %kv) = @_;
1059    
1060     my $last = \$self->{"last_$sourcetype"};
1061    
1062     $source
1063     ? ($$last = $source)
1064     : ($source = $$last);
1065    
1066     my $source = $self->{source}{$source} ||= _default_source $source;
1067    
1068     my $al = $source->{actions};
1069    
1070     push @$al, { type => "pause" }
1071     while @$al < $self->{tick} - 1;
1072    
1073     $kv{type} = $type;
1074    
1075     push @{ $source->{actions} }, \%kv;
1076    
1077     $self->{tick_duration} = $kv{duration}
1078     if $kv{duration} > $self->{tick_duration};
1079    
1080     if ($self->{tick} != @$al) {
1081     $self->{tick} = @$al;
1082     $self->{duration} += delete $self->{tick_duration};
1083     }
1084    
1085     $self
1086     }
1087 root 1.1
1088 root 1.12 =item $al = $al->pause ($duration)
1089    
1090     Creates a pause with the given duration. Makes sure that time progresses
1091     in any case, even when C<$duration> is C<0>.
1092    
1093     =cut
1094    
1095     sub pause {
1096     my ($self, $duration) = @_;
1097    
1098     $self->{tick_duration} = $duration
1099     if $duration > $self->{tick_duration};
1100    
1101     $self->{duration} += delete $self->{tick_duration};
1102    
1103     # find the source with the longest list
1104    
1105     for my $source (values %{ $self->{source} }) {
1106     if (@{ $source->{actions} } == $self->{tick}) {
1107     # this source is one of the longest
1108    
1109     # create a pause event only if $duration is non-zero...
1110     push @{ $source->{actions} }, { type => "pause", duration => $duration*1 }
1111     if $duration;
1112    
1113     # ... but advance time in any case
1114     ++$self->{tick};
1115    
1116     return $self;
1117     }
1118     }
1119    
1120     # no event sources are longest. so advance time in any case
1121     ++$self->{tick};
1122    
1123     Carp::croak "AnyEvent::WebDriver::Actions: multiple pause calls in a row not (yet) supported"
1124     if $duration;
1125    
1126     $self
1127     }
1128    
1129     =item $al = $al->pointer_down ($button, $source)
1130    
1131     =item $al = $al->pointer_up ($button, $source)
1132    
1133     Press or release the given button. C<$button> defaults to C<1>.
1134    
1135     =item $al = $al->click ($button, $source)
1136    
1137     Convenience function that creates a button press and release action
1138     without any delay between them. C<$button> defaults to C<1>.
1139    
1140     =item $al = $al->doubleclick ($button, $source)
1141    
1142     Convenience function that creates two button press and release action
1143     pairs in a row, with no unnecessary delay between them. C<$button>
1144     defaults to C<1>.
1145    
1146     =cut
1147    
1148     sub pointer_down {
1149     my ($self, $button, $source) = @_;
1150    
1151     $self->_add ($source, ptr => pointerDown => button => ($button // 1)*1)
1152     }
1153    
1154     sub pointer_up {
1155     my ($self, $button, $source) = @_;
1156    
1157     $self->_add ($source, ptr => pointerUp => button => ($button // 1)*1)
1158     }
1159    
1160     sub click {
1161     my ($self, $button, $source) = @_;
1162    
1163     $self
1164     ->pointer_down ($button, $source)
1165     ->pointer_up ($button)
1166     }
1167    
1168     sub doubleclick {
1169     my ($self, $button, $source) = @_;
1170    
1171     $self
1172     ->click ($button, $source)
1173     ->click ($button)
1174     }
1175    
1176     =item $al = $al->move ($button, $origin, $x, $y, $duration, $source)
1177    
1178     Moves a pointer to the given position, relative to origin (either
1179     "viewport", "pointer" or an element object.
1180    
1181     =cut
1182    
1183     sub move {
1184     my ($self, $origin, $x, $y, $duration, $source) = @_;
1185    
1186     $self->_add ($source, ptr => pointerMove =>
1187     origin => $origin, x => $x*1, y => $y*1, duration => $duration*1)
1188     }
1189    
1190     =item $al = $al->keyDown ($key, $source)
1191    
1192     =item $al = $al->keyUp ($key, $source)
1193    
1194     Press or release the given key.
1195    
1196     =cut
1197    
1198     sub key_down {
1199     my ($self, $key, $source) = @_;
1200    
1201     $self->_add ($source, kbd => keyDown => value => $key)
1202     }
1203    
1204     sub key_up {
1205     my ($self, $key, $source) = @_;
1206    
1207     $self->_add ($source, kbd => keyUp => value => $key)
1208     }
1209    
1210     sub key {
1211     my ($self, $key, $source) = @_;
1212    
1213     $self
1214     ->key_down ($key, $source)
1215     ->key_up ($key)
1216     }
1217    
1218     =item $al->perform ($wd)
1219    
1220     Finaluses and compiles the list, if not done yet, and calls C<<
1221     $wd->perform >> with it.
1222    
1223     If C<$wd> is undef, and the action list was created using the C<<
1224     $wd->actions >> method, then perform it against that WebDriver object.
1225    
1226     There is no underscore variant - call the C<perform_actions_> method with
1227     the action object instead.
1228    
1229     =item $al->perform_release ($wd)
1230    
1231     Exactly like C<perform>, but additionally call C<release_actions>
1232     afterwards.
1233 root 1.1
1234     =cut
1235    
1236 root 1.12 sub perform {
1237     my ($self, $wd) = @_;
1238    
1239     ($wd //= $self->{wd})->perform_actions ($self)
1240     }
1241 root 1.9
1242 root 1.12 sub perform_release {
1243     my ($self, $wd) = @_;
1244    
1245     ($wd //= $self->{wd})->perform_actions ($self);
1246     $wd->release_actions;
1247     }
1248 root 1.1
1249 root 1.12 =item ($actions, $duration) = $al->compile
1250    
1251     Finalises and compiles the list, if not done yet, and returns an actions
1252     object suitable for calls to C<< $wd->perform_actions >>. When called in
1253     list context, additionally returns the total duration of the action list.
1254    
1255     Since building large action lists can take nontrivial amounts of time,
1256     it can make sense to build an action list only once and then perform it
1257     multiple times.
1258    
1259     Actions must not be added after compiling a list.
1260 root 1.1
1261     =cut
1262    
1263 root 1.12 sub compile {
1264     my ($self) = @_;
1265    
1266     $self->{duration} += delete $self->{tick_duration};
1267    
1268     delete $self->{tick};
1269     delete $self->{last_kbd};
1270     delete $self->{last_ptr};
1271    
1272     $self->{actions} ||= [values %{ delete $self->{source} }];
1273    
1274     wantarray
1275     ? ($self->{actions}, $self->{duration})
1276     : $self->{actions}
1277 root 1.1 }
1278    
1279     =back
1280    
1281     =head2 EVENT BASED API
1282    
1283     This module wouldn't be a good AnyEvent citizen if it didn't have a true
1284     event-based API.
1285    
1286     In fact, the simplified API, as documented above, is emulated via the
1287     event-based API and an C<AUTOLOAD> function that automatically provides
1288     blocking wrappers around the callback-based API.
1289    
1290     Every method documented in the L<SIMPLIFIED API> section has an equivalent
1291     event-based method that is formed by appending a underscore (C<_>) to the
1292     method name, and appending a callback to the argument list (mnemonic: the
1293     underscore indicates the "the action is not yet finished" after the call
1294     returns).
1295    
1296     For example, instead of a blocking calls to C<new_session>, C<navigate_to>
1297     and C<back>, you can make a callback-based ones:
1298    
1299     my $cv = AE::cv;
1300    
1301     $wd->new_session ({}, sub {
1302     my ($status, $value) = @_,
1303    
1304     die "error $value->{error}" if $status ne "200";
1305    
1306     $wd->navigate_to_ ("http://www.nethype.de", sub {
1307    
1308     $wd->back_ (sub {
1309     print "all done\n";
1310     $cv->send;
1311     });
1312    
1313     });
1314     });
1315    
1316     $cv->recv;
1317    
1318     While the blocking methods C<croak> on errors, the callback-based ones all
1319     pass two values to the callback, C<$status> and C<$res>, where C<$status>
1320 root 1.9 is the HTTP status code (200 for successful requests, typically 4xx or
1321 root 1.1 5xx for errors), and C<$res> is the value of the C<value> key in the JSON
1322     response object.
1323    
1324     Other than that, the underscore variants and the blocking variants are
1325     identical.
1326    
1327     =head2 LOW LEVEL API
1328    
1329 root 1.9 All the simplified API methods are very thin wrappers around WebDriver
1330     commands of the same name. They are all implemented in terms of the
1331 root 1.1 low-level methods (C<req>, C<get>, C<post> and C<delete>), which exists
1332     in blocking and callback-based variants (C<req_>, C<get_>, C<post_> and
1333     C<delete_>).
1334    
1335     Examples are after the function descriptions.
1336    
1337     =over
1338    
1339     =item $wd->req_ ($method, $uri, $body, $cb->($status, $value))
1340    
1341     =item $value = $wd->req ($method, $uri, $body)
1342    
1343     Appends the C<$uri> to the C<endpoint/session/{sessionid}/> URL and makes
1344     a HTTP C<$method> request (C<GET>, C<POST> etc.). C<POST> requests can
1345     provide a UTF-8-encoded JSON text as HTTP request body, or the empty
1346     string to indicate no body is used.
1347    
1348     For the callback version, the callback gets passed the HTTP status code
1349     (200 for every successful request), and the value of the C<value> key in
1350     the JSON response object as second argument.
1351    
1352     =item $wd->get_ ($uri, $cb->($status, $value))
1353    
1354     =item $value = $wd->get ($uri)
1355    
1356     Simply a call to C<req_> with C<$method> set to C<GET> and an empty body.
1357    
1358     =item $wd->post_ ($uri, $data, $cb->($status, $value))
1359    
1360     =item $value = $wd->post ($uri, $data)
1361    
1362     Simply a call to C<req_> with C<$method> set to C<POST> - if C<$body> is
1363     C<undef>, then an empty object is send, otherwise, C<$data> must be a
1364     valid request object, which gets encoded into JSON for you.
1365    
1366     =item $wd->delete_ ($uri, $cb->($status, $value))
1367    
1368     =item $value = $wd->delete ($uri)
1369    
1370     Simply a call to C<req_> with C<$method> set to C<DELETE> and an empty body.
1371    
1372     =cut
1373    
1374     =back
1375    
1376     Example: implement C<get_all_cookies>, which is a simple C<GET> request
1377     without any parameters:
1378    
1379     $cookies = $wd->get ("cookie");
1380    
1381     Example: implement C<execute_script>, which needs some parameters:
1382    
1383     $results = $wd->post ("execute/sync" => { script => "$javascript", args => [] });
1384    
1385 root 1.12 Example: call C<find_elements> to find all C<IMG> elements:
1386 root 1.1
1387 root 1.12 $elems = $wd->post (elements => { using => "css selector", value => "img" });
1388 root 1.1
1389     =cut
1390    
1391     =head1 HISTORY
1392    
1393 root 1.2 This module was unintentionally created (it started inside some quickly
1394     hacked-together script) simply because I couldn't get the existing
1395 root 1.1 C<Selenium::Remote::Driver> module to work, ever, despite multiple
1396     attempts over the years and trying to report multiple bugs, which have
1397     been completely ignored. It's also not event-based, so, yeah...
1398    
1399     =head1 AUTHOR
1400    
1401     Marc Lehmann <schmorp@schmorp.de>
1402     http://anyevent.schmorp.de
1403    
1404     =cut
1405    
1406     1
1407