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

File Contents

# Content
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 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
37 To make most of this module, or, in fact, to make any reasonable use of
38 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
41 https://www.w3.org/TR/webdriver1/
42
43 =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 =cut
49
50 package AnyEvent::WebDriver;
51
52 use common::sense;
53
54 use Carp ();
55 use JSON::XS ();
56 use AnyEvent ();
57 use AnyEvent::HTTP ();
58
59 our $VERSION = 0.2;
60
61 our $WEB_ELEMENT_IDENTIFIER = "element-6066-11e4-a52e-4f735466cecf";
62
63 my $json = JSON::XS->new
64 ->utf8;
65
66 $json->boolean_values (0, 1)
67 if $json->can ("boolean_values");
68
69 sub req_ {
70 my ($self, $method, $ep, $body, $cb) = @_;
71
72 AnyEvent::HTTP::http_request $method => "$self->{_ep}$ep",
73 body => $body,
74 timeout => $self->{timeout},
75 headers => { "content-type" => "application/json; charset=utf-8", "cache-control" => "no-cache" },
76 ($self->{proxy} eq "default" ? () : (proxy => $self->{proxy})),
77 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 my ($self, $ep, $cb) = @_;
90
91 $self->req_ (GET => $ep, undef, $cb)
92 }
93
94 sub post_ {
95 my ($self, $ep, $data, $cb) = @_;
96
97 $self->req_ (POST => $ep, $json->encode ($data || {}), $cb)
98 }
99
100 sub delete_ {
101 my ($self, $ep, $cb) = @_;
102
103 $self->req_ (DELETE => $ep, "", $cb)
104 }
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 =head2 WEBDRIVER OBJECTS
145
146 =over
147
148 =item new AnyEvent::WebDriver key => value...
149
150 Create a new WebDriver object. Example for a remote WebDriver connection
151 (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 =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 is not an overall request timeout. Also, individual requests might extend
181 this timeout if they are known to take longer.
182
183 =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 timeout => 300,
195 %kv,
196 }, $class
197 }
198
199 sub DESTROY {
200 my ($self) = @_;
201
202 $self->delete_session
203 if exists $self->{sid};
204 }
205
206 =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 =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 The method names are pretty much taken directly from the W3C WebDriver
228 specification, e.g. the request documented in the "Get All Cookies"
229 section is implemented via the C<get_all_cookies> method.
230
231 The order is the same as in the WebDriver draft at the time of this
232 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 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 (e.g. C<capabilities>).
246
247 No session-dependent methods must be called before this function returns
248 successfully, and only one session can be created per WebDriver object.
249
250 On success, C<< $wd->{sid} >> is set to the session ID, and C<<
251 $wd->{capabilities} >> is set to the returned capabilities.
252
253 my $wd = new AnyEvent::Selenium endpoint => "http://localhost:4545";
254
255 $wd->new_session ({
256 capabilities => {
257 pageLoadStrategy => "normal",
258 }.
259 });
260
261 =cut
262
263 sub new_session_ {
264 my ($self, $kv, $cb) = @_;
265
266 local $self->{_ep} = "$self->{endpoint}/";
267 $self->post_ (session => $kv, sub {
268 my ($status, $res) = @_;
269
270 if ($status eq "200") {
271 $self->{sid} = $res->{sessionId};
272 $self->{capabilities} = $res->{capabilities};
273
274 $self->{_ep} = "$self->{endpoint}/session/$self->{sid}/";
275 }
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 my ($self, $cb) = @_;
290
291 local $self->{_ep} = "$self->{endpoint}/session/$self->{sid}";
292 $self->delete_ ("" => $cb);
293 }
294
295 =item $timeouts = $wd->get_timeouts
296
297 Get the current timeouts, e.g.:
298
299 my $timeouts = $wd->get_timeouts;
300 => { implicit => 0, pageLoad => 300000, script => 30000 }
301
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 Queries the current page URL as set by C<navigate_to>.
333
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 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 select the nth subframe, or an element object.
429
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 => { height => 1040, width => 540, x => 0, y => 0 }
452
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 Changes the window size by either maximising, minimising or making it
474 fullscreen. In my experience, this will timeout if no window manager is
475 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 =item $element = $wd->find_element ($location_strategy, $selector)
500
501 Finds the first element specified by the given selector and returns its
502 element object. Raises an error when no element was found.
503
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 => e.g. { "element-6066-11e4-a52e-4f735466cecf" => "decddca8-5986-4e1d-8c93-efe952505a5f" }
510
511 =item $elements = $wd->find_elements ($location_strategy, $selector)
512
513 As above, but returns an arrayref of all found element objects.
514
515 =item $element = $wd->find_element_from_element ($element, $location_strategy, $selector)
516
517 Like C<find_element>, but looks only inside the specified C<$element>.
518
519 =item $elements = $wd->find_elements_from_element ($element, $location_strategy, $selector)
520
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 =item $element = $wd->get_active_element
527
528 Returns the active element.
529
530 =cut
531
532 sub find_element_ {
533 $_[0]->post_ (element => { using => "$_[1]", value => "$_[2]" }, $_[3]);
534 }
535
536 sub find_elements_ {
537 $_[0]->post_ (elements => { using => "$_[1]", value => "$_[2]" }, $_[3]);
538 }
539
540 sub find_element_from_element_ {
541 $_[0]->post_ ("element/$_[1]/element" => { using => "$_[2]", value => "$_[3]" }, $_[4]);
542 }
543
544 sub find_elements_from_element_ {
545 $_[0]->post_ ("element/$_[1]/elements" => { using => "$_[2]", value => "$_[3]" }, $_[4]);
546 }
547
548 sub get_active_element_ {
549 $_[0]->get_ ("element/active" => $_[1]);
550 }
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 =item $string = $wd->get_element_attribute ($element, $name)
565
566 Returns the value of the given attribute.
567
568 =item $string = $wd->get_element_property ($element, $name)
569
570 Returns the value of the given property.
571
572 =item $string = $wd->get_element_css_value ($element, $name)
573
574 Returns the value of the given CSS value.
575
576 =item $string = $wd->get_element_text ($element)
577
578 Returns the (rendered) text content of the given element.
579
580 =item $string = $wd->get_element_tag_name ($element)
581
582 Returns the tag of the given element.
583
584 =item $rect = $wd->get_element_rect ($element)
585
586 Returns the element rect(angle) of the given element.
587
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 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/selected" => $_[2]);
596 }
597
598 sub get_element_attribute_ {
599 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/attribute/$_[2]" => $_[3]);
600 }
601
602 sub get_element_property_ {
603 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/property/$_[2]" => $_[3]);
604 }
605
606 sub get_element_css_value_ {
607 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/css/$_[2]" => $_[3]);
608 }
609
610 sub get_element_text_ {
611 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/text" => $_[2]);
612 }
613
614 sub get_element_tag_name_ {
615 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/name" => $_[2]);
616 }
617
618 sub get_element_rect_ {
619 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/rect" => $_[2]);
620 }
621
622 sub is_element_enabled_ {
623 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/enabled" => $_[2]);
624 }
625
626 =back
627
628 =head3 ELEMENT INTERACTION
629
630 =over
631
632 =cut
633
634 =item $wd->element_click ($element)
635
636 Clicks the given element.
637
638 =item $wd->element_clear ($element)
639
640 Clear the contents of the given element.
641
642 =item $wd->element_send_keys ($element, $text)
643
644 Sends the given text as key events to the given element.
645
646 =cut
647
648 sub element_click_ {
649 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/click" => undef, $_[2]);
650 }
651
652 sub element_clear_ {
653 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/clear" => undef, $_[2]);
654 }
655
656 sub element_send_keys_ {
657 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/value" => { text => "$_[2]" }, $_[3]);
658 }
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 user activity, or an C<AnyEvent::WebDriver::Actions> object). For further
768 details, read the spec or the section L<ACTION LISTS>, below.
769
770 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
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 { type => "pointerMove", duration => 100, origin => $input, x => 40, y => 5 },
782 { 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 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 =item $wd->release_actions
829
830 Release all keys and pointer buttons currently depressed.
831
832 =cut
833
834 sub perform_actions_ {
835 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 }
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 Create a screenshot, returning it as a PNG image in a C<data:> URL.
902
903 =item $wd->take_element_screenshot ($element)
904
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 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/screenshot" => $_[2]);
915 }
916
917 =back
918
919 =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
1088 =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
1234 =cut
1235
1236 sub perform {
1237 my ($self, $wd) = @_;
1238
1239 ($wd //= $self->{wd})->perform_actions ($self)
1240 }
1241
1242 sub perform_release {
1243 my ($self, $wd) = @_;
1244
1245 ($wd //= $self->{wd})->perform_actions ($self);
1246 $wd->release_actions;
1247 }
1248
1249 =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
1261 =cut
1262
1263 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 }
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 is the HTTP status code (200 for successful requests, typically 4xx or
1321 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 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 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 Example: call C<find_elements> to find all C<IMG> elements:
1386
1387 $elems = $wd->post (elements => { using => "css selector", value => "img" });
1388
1389 =cut
1390
1391 =head1 HISTORY
1392
1393 This module was unintentionally created (it started inside some quickly
1394 hacked-together script) simply because I couldn't get the existing
1395 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