ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-WebDriver/WebDriver.pm
Revision: 1.13
Committed: Wed Aug 29 02:47:02 2018 UTC (5 years, 10 months ago) by root
Branch: MAIN
Changes since 1.12: +152 -19 lines
Log Message:
*** empty log message ***

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 To reduce typing and memory strain, the element finding functions accept
496 some shorter and hopefully easier to remember aliases for the standard
497 locator strategy values, as follows:
498
499 Alias Locator Strategy
500 css css selector
501 link link text
502 substr partial link text
503 tag tag name
504
505 =over
506
507 =cut
508
509 our %USING = (
510 css => "css selector",
511 link => "link text",
512 substr => "partial link text",
513 tag => "tag name",
514 );
515
516 sub _using($) {
517 using => $USING{$_[0]} // "$_[0]"
518 }
519
520 =item $element = $wd->find_element ($locator_strategy, $selector)
521
522 Finds the first element specified by the given selector and returns its
523 element object. Raises an error when no element was found.
524
525 $element = $wd->find_element ("css selector" => "body a");
526 $element = $wd->find_element ("link text" => "Click Here For Porn");
527 $element = $wd->find_element ("partial link text" => "orn");
528 $element = $wd->find_element ("tag name" => "input");
529 $element = $wd->find_element ("xpath" => '//input[@type="text"]');
530 => e.g. { "element-6066-11e4-a52e-4f735466cecf" => "decddca8-5986-4e1d-8c93-efe952505a5f" }
531
532 =item $elements = $wd->find_elements ($locator_strategy, $selector)
533
534 As above, but returns an arrayref of all found element objects.
535
536 =item $element = $wd->find_element_from_element ($element, $locator_strategy, $selector)
537
538 Like C<find_element>, but looks only inside the specified C<$element>.
539
540 =item $elements = $wd->find_elements_from_element ($element, $locator_strategy, $selector)
541
542 Like C<find_elements>, but looks only inside the specified C<$element>.
543
544 my $head = $wd->find_element ("tag name" => "head");
545 my $links = $wd->find_elements_from_element ($head, "tag name", "link");
546
547 =item $element = $wd->get_active_element
548
549 Returns the active element.
550
551 =cut
552
553 sub find_element_ {
554 $_[0]->post_ (element => { _using $_[1], value => "$_[2]" }, $_[3]);
555 }
556
557 sub find_elements_ {
558 $_[0]->post_ (elements => { _using $_[1], value => "$_[2]" }, $_[3]);
559 }
560
561 sub find_element_from_element_ {
562 $_[0]->post_ ("element/$_[1]/element" => { _using $_[2], value => "$_[3]" }, $_[4]);
563 }
564
565 sub find_elements_from_element_ {
566 $_[0]->post_ ("element/$_[1]/elements" => { _using $_[2], value => "$_[3]" }, $_[4]);
567 }
568
569 sub get_active_element_ {
570 $_[0]->get_ ("element/active" => $_[1]);
571 }
572
573 =back
574
575 =head3 ELEMENT STATE
576
577 =over
578
579 =cut
580
581 =item $bool = $wd->is_element_selected
582
583 Returns whether the given input or option element is selected or not.
584
585 =item $string = $wd->get_element_attribute ($element, $name)
586
587 Returns the value of the given attribute.
588
589 =item $string = $wd->get_element_property ($element, $name)
590
591 Returns the value of the given property.
592
593 =item $string = $wd->get_element_css_value ($element, $name)
594
595 Returns the value of the given CSS value.
596
597 =item $string = $wd->get_element_text ($element)
598
599 Returns the (rendered) text content of the given element.
600
601 =item $string = $wd->get_element_tag_name ($element)
602
603 Returns the tag of the given element.
604
605 =item $rect = $wd->get_element_rect ($element)
606
607 Returns the element rect(angle) of the given element.
608
609 =item $bool = $wd->is_element_enabled
610
611 Returns whether the element is enabled or not.
612
613 =cut
614
615 sub is_element_selected_ {
616 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/selected" => $_[2]);
617 }
618
619 sub get_element_attribute_ {
620 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/attribute/$_[2]" => $_[3]);
621 }
622
623 sub get_element_property_ {
624 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/property/$_[2]" => $_[3]);
625 }
626
627 sub get_element_css_value_ {
628 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/css/$_[2]" => $_[3]);
629 }
630
631 sub get_element_text_ {
632 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/text" => $_[2]);
633 }
634
635 sub get_element_tag_name_ {
636 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/name" => $_[2]);
637 }
638
639 sub get_element_rect_ {
640 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/rect" => $_[2]);
641 }
642
643 sub is_element_enabled_ {
644 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/enabled" => $_[2]);
645 }
646
647 =back
648
649 =head3 ELEMENT INTERACTION
650
651 =over
652
653 =cut
654
655 =item $wd->element_click ($element)
656
657 Clicks the given element.
658
659 =item $wd->element_clear ($element)
660
661 Clear the contents of the given element.
662
663 =item $wd->element_send_keys ($element, $text)
664
665 Sends the given text as key events to the given element.
666
667 =cut
668
669 sub element_click_ {
670 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/click" => undef, $_[2]);
671 }
672
673 sub element_clear_ {
674 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/clear" => undef, $_[2]);
675 }
676
677 sub element_send_keys_ {
678 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/value" => { text => "$_[2]" }, $_[3]);
679 }
680
681 =back
682
683 =head3 DOCUMENT HANDLING
684
685 =over
686
687 =cut
688
689 =item $source = $wd->get_page_source
690
691 Returns the (HTML/XML) page source of the current document.
692
693 =item $results = $wd->execute_script ($javascript, $args)
694
695 Synchronously execute the given script with given arguments and return its
696 results (C<$args> can be C<undef> if no arguments are wanted/needed).
697
698 $ten = $wd->execute_script ("return arguments[0]+arguments[1]", [3, 7]);
699
700 =item $results = $wd->execute_async_script ($javascript, $args)
701
702 Similar to C<execute_script>, but doesn't wait for script to return, but
703 instead waits for the script to call its last argument, which is added to
704 C<$args> automatically.
705
706 $twenty = $wd->execute_async_script ("arguments[0](20)", undef);
707
708 =cut
709
710 sub get_page_source_ {
711 $_[0]->get_ (source => $_[1]);
712 }
713
714 sub execute_script_ {
715 $_[0]->post_ ("execute/sync" => { script => "$_[1]", args => $_[2] || [] }, $_[3]);
716 }
717
718 sub execute_async_script_ {
719 $_[0]->post_ ("execute/async" => { script => "$_[1]", args => $_[2] || [] }, $_[3]);
720 }
721
722 =back
723
724 =head3 COOKIES
725
726 =over
727
728 =cut
729
730 =item $cookies = $wd->get_all_cookies
731
732 Returns all cookies, as an arrayref of hashrefs.
733
734 # google surely sets a lot of cookies without my consent
735 $wd->navigate_to ("http://google.com");
736 use Data::Dump;
737 ddx $wd->get_all_cookies;
738
739 =item $cookie = $wd->get_named_cookie ($name)
740
741 Returns a single cookie as a hashref.
742
743 =item $wd->add_cookie ($cookie)
744
745 Adds the given cookie hashref.
746
747 =item $wd->delete_cookie ($name)
748
749 Delete the named cookie.
750
751 =item $wd->delete_all_cookies
752
753 Delete all cookies.
754
755 =cut
756
757 sub get_all_cookies_ {
758 $_[0]->get_ (cookie => $_[1]);
759 }
760
761 sub get_named_cookie_ {
762 $_[0]->get_ ("cookie/$_[1]" => $_[2]);
763 }
764
765 sub add_cookie_ {
766 $_[0]->post_ (cookie => { cookie => $_[1] }, $_[2]);
767 }
768
769 sub delete_cookie_ {
770 $_[0]->delete_ ("cookie/$_[1]" => $_[2]);
771 }
772
773 sub delete_all_cookies_ {
774 $_[0]->delete_ (cookie => $_[2]);
775 }
776
777 =back
778
779 =head3 ACTIONS
780
781 =over
782
783 =cut
784
785 =item $wd->perform_actions ($actions)
786
787 Perform the given actions (an arrayref of action specifications simulating
788 user activity, or an C<AnyEvent::WebDriver::Actions> object). For further
789 details, read the spec or the section L<ACTION LISTS>, below.
790
791 An example to get you started (see the next example for a mostly
792 equivalent example using the C<AnyEvent::WebDriver::Actions> helper API):
793
794 $wd->navigate_to ("https://duckduckgo.com/html");
795 my $input = $wd->find_element ("css selector", 'input[type="text"]');
796 $wd->perform_actions ([
797 {
798 id => "myfatfinger",
799 type => "pointer",
800 pointerType => "touch",
801 actions => [
802 { type => "pointerMove", duration => 100, origin => $input, x => 40, y => 5 },
803 { type => "pointerDown", button => 1 },
804 { type => "pause", duration => 40 },
805 { type => "pointerUp", button => 1 },
806 ],
807 },
808 {
809 id => "mykeyboard",
810 type => "key",
811 actions => [
812 { type => "pause" },
813 { type => "pause" },
814 { type => "pause" },
815 { type => "pause" },
816 { type => "keyDown", value => "a" },
817 { type => "pause", duration => 100 },
818 { type => "keyUp", value => "a" },
819 { type => "pause", duration => 100 },
820 { type => "keyDown", value => "b" },
821 { type => "pause", duration => 100 },
822 { type => "keyUp", value => "b" },
823 { type => "pause", duration => 2000 },
824 { type => "keyDown", value => "\x{E007}" }, # enter
825 { type => "pause", duration => 100 },
826 { type => "keyUp", value => "\x{E007}" }, # enter
827 { type => "pause", duration => 5000 },
828 ],
829 },
830 ]);
831
832 And here is essentially the same (except for fewer pauses) example as
833 above, using the much simpler C<AnyEvent::WebDriver::Actions> API. Note
834 that the pointer up and key down event happen concurrently in this
835 example:
836
837 $wd->navigate_to ("https://duckduckgo.com/html");
838 my $input = $wd->find_element ("css selector", 'input[type="text"]');
839 $wd->actions
840 ->move ($input, 40, 5, "touch1")
841 ->click;
842 ->key ("a");
843 ->key ("b");
844 ->pause (2000);
845 ->key ("\x{E007}")
846 ->pause (5000);
847 ->perform;
848
849 =item $wd->release_actions
850
851 Release all keys and pointer buttons currently depressed.
852
853 =cut
854
855 sub perform_actions_ {
856 if (UNIVERSAL::isa $_[1], AnyEvent::WebDriver::Actions::) {
857 my ($actions, $duration) = $_[1]->compile;
858 local $_[0]{timeout} = $_[0]{timeout} + $duration * 1e-3;
859 $_[0]->post_ (actions => { actions => $actions }, $_[2]);
860 } else {
861 $_[0]->post_ (actions => { actions => $_[1] }, $_[2]);
862 }
863 }
864
865 sub release_actions_ {
866 $_[0]->delete_ (actions => $_[1]);
867 }
868
869 =back
870
871 =head3 USER PROMPTS
872
873 =over
874
875 =cut
876
877 =item $wd->dismiss_alert
878
879 Dismiss a simple dialog, if present.
880
881 =item $wd->accept_alert
882
883 Accept a simple dialog, if present.
884
885 =item $text = $wd->get_alert_text
886
887 Returns the text of any simple dialog.
888
889 =item $text = $wd->send_alert_text
890
891 Fills in the user prompt with the given text.
892
893
894 =cut
895
896 sub dismiss_alert_ {
897 $_[0]->post_ ("alert/dismiss" => undef, $_[1]);
898 }
899
900 sub accept_alert_ {
901 $_[0]->post_ ("alert/accept" => undef, $_[1]);
902 }
903
904 sub get_alert_text_ {
905 $_[0]->get_ ("alert/text" => $_[1]);
906 }
907
908 sub send_alert_text_ {
909 $_[0]->post_ ("alert/text" => { text => "$_[1]" }, $_[2]);
910 }
911
912 =back
913
914 =head3 SCREEN CAPTURE
915
916 =over
917
918 =cut
919
920 =item $wd->take_screenshot
921
922 Create a screenshot, returning it as a PNG image in a C<data:> URL.
923
924 =item $wd->take_element_screenshot ($element)
925
926 Accept a simple dialog, if present.
927
928 =cut
929
930 sub take_screenshot_ {
931 $_[0]->get_ (screenshot => $_[1]);
932 }
933
934 sub take_element_screenshot_ {
935 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/screenshot" => $_[2]);
936 }
937
938 =back
939
940 =head2 ACTION LISTS
941
942 Action lists can be quite complicated. Or at least it took a while for
943 me to twist my head around them. Basically, an action list consists of a
944 number of sources representing devices (such as a finger, a mouse, a pen
945 or a keyboard) and a list of actions for each source.
946
947 An action can be a key press, a pointer move or a pause (time
948 delay). Actions from different sources can happen "at the same time",
949 while actions from a single source are executed in order.
950
951 While you can provide an action list manually, it is (hopefully) less
952 cumbersome to use the API described in this section to create them.
953
954 The basic process of creating and performing actions is to create a new
955 action list, adding action sources, followed by adding actions. Finally
956 you would C<perform> those actions on the WebDriver.
957
958 Virtual time progresses as long as you add actions to the same event
959 source. Adding events to different sources are considered to happen
960 concurrently. If you want to force time to progress, you can do this using
961 a call to C<< ->pause (0) >>.
962
963 Most methods here are designed to chain, i.e. they return the web actions
964 object, to simplify multiple calls.
965
966 For example, to simulate a mouse click to an input element, followed by
967 entering some text and pressing enter, you can use this:
968
969 $wd->actions
970 ->click (1, 100)
971 ->type ("some text")
972 ->key ("{Enter}")
973 ->perform;
974
975 By default, keyboard and mouse input sources are provided. You can create
976 your own sources and use them when adding events. The above example could
977 be more verbosely written like this:
978
979 $wd->actions
980 ->click (1, 100, "mouse")
981 ->type ("some text")
982 ->key ("{Enter}")
983 ->perform;
984
985 When you specify the event source expliticly it will switch the current
986 "focus" for this class of device (all keyboards are in one class, all
987 pointer-like devices such as mice/fingers/pens are in one class), so you
988 don't have to specify the source for subsequent actions.
989
990 When you use the sources C<keyboard>, C<mouse>, C<touch1>..C<touch3>,
991 C<pen> without defining them, then a suitable default source will be
992 created for them.
993
994 =over 4
995
996 =cut
997
998 package AnyEvent::WebDriver::Actions;
999
1000 =item $al = new AnyEvent::WebDriver::Actions
1001
1002 Create a new empty action list object. More often you would use the C<<
1003 $wd->action_list >> method to create one that is already associated with
1004 a given web driver.
1005
1006 =cut
1007
1008 sub new {
1009 my ($class, %kv) = @_;
1010
1011 $kv{last_kbd} = "keyboard";
1012 $kv{last_ptr} = "mouse";
1013
1014 bless \%kv, $class
1015 }
1016
1017 =item $al = $al->source ($id, $type, key => value...)
1018
1019 The first time you call this with a givne ID, this defines the event
1020 source using the extra parameters. Subsequent calls merely switch the
1021 current source for its event class.
1022
1023 It's not an error to define built-in sources (such as C<keyboard> or
1024 C<touch1>) differently then the defaults.
1025
1026 Example: define a new touch device called C<fatfinger>.
1027
1028 $al->source (fatfinger => "pointer", pointerType => "touch");
1029
1030 Example: switchdefine a new touch device called C<fatfinger>.
1031
1032 $al->source (fatfinger => "pointer", pointerType => "touch");
1033
1034 =cut
1035
1036 sub _default_source($) {
1037 my ($source) = @_;
1038
1039 $source eq "keyboard" ? { actions => [], id => $source, type => "key" }
1040 : $source eq "mouse" ? { actions => [], id => $source, type => "pointer", pointerType => "mouse" }
1041 : $source eq "touch" ? { actions => [], id => $source, type => "pointer", pointerType => "touch" }
1042 : $source eq "pen" ? { actions => [], id => $source, type => "pointer", pointerType => "pen" }
1043 : Carp::croak "AnyEvent::WebDriver::Actions: event source '$source' not defined"
1044 }
1045
1046 my %source_class = (
1047 key => "kbd",
1048 pointer => "ptr",
1049 );
1050
1051 sub source {
1052 my ($self, $id, $type, %kv) = @_;
1053
1054 if (defined $type) {
1055 !exists $self->{source}{$id}
1056 or Carp::croak "AnyEvent::WebDriver::Actions: source '$id' already defined";
1057
1058 $kv{id} = $id;
1059 $kv{type} = $type;
1060 $kv{actions} = [];
1061
1062 $self->{source}{$id} = \%kv;
1063 }
1064
1065 my $source = $self->{source}{$id} ||= _default_source $id;
1066
1067 my $last = $source_class{$source->{type}} // "xxx";
1068
1069 $self->{"last_$last"} = $id;
1070
1071 $self
1072 }
1073
1074 sub _add {
1075 my ($self, $source, $sourcetype, $type, %kv) = @_;
1076
1077 my $last = \$self->{"last_$sourcetype"};
1078
1079 $source
1080 ? ($$last = $source)
1081 : ($source = $$last);
1082
1083 my $source = $self->{source}{$source} ||= _default_source $source;
1084
1085 my $al = $source->{actions};
1086
1087 push @$al, { type => "pause" }
1088 while @$al < $self->{tick} - 1;
1089
1090 $kv{type} = $type;
1091
1092 push @{ $source->{actions} }, \%kv;
1093
1094 $self->{tick_duration} = $kv{duration}
1095 if $kv{duration} > $self->{tick_duration};
1096
1097 if ($self->{tick} != @$al) {
1098 $self->{tick} = @$al;
1099 $self->{duration} += delete $self->{tick_duration};
1100 }
1101
1102 $self
1103 }
1104
1105 =item $al = $al->pause ($duration)
1106
1107 Creates a pause with the given duration. Makes sure that time progresses
1108 in any case, even when C<$duration> is C<0>.
1109
1110 =cut
1111
1112 sub pause {
1113 my ($self, $duration) = @_;
1114
1115 $self->{tick_duration} = $duration
1116 if $duration > $self->{tick_duration};
1117
1118 $self->{duration} += delete $self->{tick_duration};
1119
1120 # find the source with the longest list
1121
1122 for my $source (values %{ $self->{source} }) {
1123 if (@{ $source->{actions} } == $self->{tick}) {
1124 # this source is one of the longest
1125
1126 # create a pause event only if $duration is non-zero...
1127 push @{ $source->{actions} }, { type => "pause", duration => $duration*1 }
1128 if $duration;
1129
1130 # ... but advance time in any case
1131 ++$self->{tick};
1132
1133 return $self;
1134 }
1135 }
1136
1137 # no event sources are longest. so advance time in any case
1138 ++$self->{tick};
1139
1140 Carp::croak "AnyEvent::WebDriver::Actions: multiple pause calls in a row not (yet) supported"
1141 if $duration;
1142
1143 $self
1144 }
1145
1146 =item $al = $al->pointer_down ($button, $source)
1147
1148 =item $al = $al->pointer_up ($button, $source)
1149
1150 Press or release the given button. C<$button> defaults to C<1>.
1151
1152 =item $al = $al->click ($button, $source)
1153
1154 Convenience function that creates a button press and release action
1155 without any delay between them. C<$button> defaults to C<1>.
1156
1157 =item $al = $al->doubleclick ($button, $source)
1158
1159 Convenience function that creates two button press and release action
1160 pairs in a row, with no unnecessary delay between them. C<$button>
1161 defaults to C<1>.
1162
1163 =cut
1164
1165 sub pointer_down {
1166 my ($self, $button, $source) = @_;
1167
1168 $self->_add ($source, ptr => pointerDown => button => ($button // 1)*1)
1169 }
1170
1171 sub pointer_up {
1172 my ($self, $button, $source) = @_;
1173
1174 $self->_add ($source, ptr => pointerUp => button => ($button // 1)*1)
1175 }
1176
1177 sub click {
1178 my ($self, $button, $source) = @_;
1179
1180 $self
1181 ->pointer_down ($button, $source)
1182 ->pointer_up ($button)
1183 }
1184
1185 sub doubleclick {
1186 my ($self, $button, $source) = @_;
1187
1188 $self
1189 ->click ($button, $source)
1190 ->click ($button)
1191 }
1192
1193 =item $al = $al->move ($button, $origin, $x, $y, $duration, $source)
1194
1195 Moves a pointer to the given position, relative to origin (either
1196 "viewport", "pointer" or an element object.
1197
1198 =cut
1199
1200 sub move {
1201 my ($self, $origin, $x, $y, $duration, $source) = @_;
1202
1203 $self->_add ($source, ptr => pointerMove =>
1204 origin => $origin, x => $x*1, y => $y*1, duration => $duration*1)
1205 }
1206
1207 =item $al = $al->keyDown ($key, $source)
1208
1209 =item $al = $al->keyUp ($key, $source)
1210
1211 Press or release the given key.
1212
1213 =item $al = $al->key ($key, $source)
1214
1215 Peess and release the given key, without unnecessary delay.
1216
1217 A special syntax, C<{keyname}> can be used for special keys - all the special key names from
1218 L<section 17.4.2|https://www.w3.org/TR/webdriver1/#keyboard-actions> of the WebDriver recommendation
1219 can be used.
1220
1221 Example: press and release "a".
1222
1223 $al->key ("a");
1224
1225 Example: press and release the "Enter" key:
1226
1227 $al->key ("\x{e007}");
1228
1229 Example: press and release the "enter" key using the special key name syntax:
1230
1231 $al->key ("{Enter}");
1232
1233 =item $al = $al->type ($string, $source)
1234
1235 Convenience method to simulate a series of key press and release events
1236 for the keys in C<$string>. There is no syntax for special keys,
1237 everything will be typed "as-is" if possible.
1238
1239 =cut
1240
1241 our %SPECIAL_KEY = (
1242 "Unidentified" => 0xE000,
1243 "Cancel" => 0xE001,
1244 "Help" => 0xE002,
1245 "Backspace" => 0xE003,
1246 "Tab" => 0xE004,
1247 "Clear" => 0xE005,
1248 "Return" => 0xE006,
1249 "Enter" => 0xE007,
1250 "Shift" => 0xE008,
1251 "Control" => 0xE009,
1252 "Alt" => 0xE00A,
1253 "Pause" => 0xE00B,
1254 "Escape" => 0xE00C,
1255 " " => 0xE00D,
1256 "PageUp" => 0xE00E,
1257 "PageDown" => 0xE00F,
1258 "End" => 0xE010,
1259 "Home" => 0xE011,
1260 "ArrowLeft" => 0xE012,
1261 "ArrowUp" => 0xE013,
1262 "ArrowRight" => 0xE014,
1263 "ArrowDown" => 0xE015,
1264 "Insert" => 0xE016,
1265 "Delete" => 0xE017,
1266 ";" => 0xE018,
1267 "=" => 0xE019,
1268 "0" => 0xE01A,
1269 "1" => 0xE01B,
1270 "2" => 0xE01C,
1271 "3" => 0xE01D,
1272 "4" => 0xE01E,
1273 "5" => 0xE01F,
1274 "6" => 0xE020,
1275 "7" => 0xE021,
1276 "8" => 0xE022,
1277 "9" => 0xE023,
1278 "*" => 0xE024,
1279 "+" => 0xE025,
1280 "," => 0xE026,
1281 "-" => 0xE027,
1282 "." => 0xE028,
1283 "/" => 0xE029,
1284 "F1" => 0xE031,
1285 "F2" => 0xE032,
1286 "F3" => 0xE033,
1287 "F4" => 0xE034,
1288 "F5" => 0xE035,
1289 "F6" => 0xE036,
1290 "F7" => 0xE037,
1291 "F8" => 0xE038,
1292 "F9" => 0xE039,
1293 "F10" => 0xE03A,
1294 "F11" => 0xE03B,
1295 "F12" => 0xE03C,
1296 "Meta" => 0xE03D,
1297 "ZenkakuHankaku" => 0xE040,
1298 "Shift" => 0xE050,
1299 "Control" => 0xE051,
1300 "Alt" => 0xE052,
1301 "Meta" => 0xE053,
1302 "PageUp" => 0xE054,
1303 "PageDown" => 0xE055,
1304 "End" => 0xE056,
1305 "Home" => 0xE057,
1306 "ArrowLeft" => 0xE058,
1307 "ArrowUp" => 0xE059,
1308 "ArrowRight" => 0xE05A,
1309 "ArrowDown" => 0xE05B,
1310 "Insert" => 0xE05C,
1311 "Delete" => 0xE05D,
1312 );
1313
1314 sub _kv($) {
1315 $_[0] =~ /^\{(.*)\}$/s
1316 ? (exists $SPECIAL_KEY{$1}
1317 ? chr $SPECIAL_KEY{$1}
1318 : Carp::croak "AnyEvent::WebDriver::Actions: special key '$1' not known")
1319 : $_[0]
1320 }
1321
1322 sub key_down {
1323 my ($self, $key, $source) = @_;
1324
1325 $self->_add ($source, kbd => keyDown => value => _kv $key)
1326 }
1327
1328 sub key_up {
1329 my ($self, $key, $source) = @_;
1330
1331 $self->_add ($source, kbd => keyUp => value => _kv $key)
1332 }
1333
1334 sub key {
1335 my ($self, $key, $source) = @_;
1336
1337 $self
1338 ->key_down ($key, $source)
1339 ->key_up ($key)
1340 }
1341
1342 sub type {
1343 my ($self, $string, $source) = @_;
1344
1345 $self->key ($_, $source)
1346 for $string =~ /(\X)/g;
1347
1348 $self
1349 }
1350
1351 =item $al->perform ($wd)
1352
1353 Finaluses and compiles the list, if not done yet, and calls C<<
1354 $wd->perform >> with it.
1355
1356 If C<$wd> is undef, and the action list was created using the C<<
1357 $wd->actions >> method, then perform it against that WebDriver object.
1358
1359 There is no underscore variant - call the C<perform_actions_> method with
1360 the action object instead.
1361
1362 =item $al->perform_release ($wd)
1363
1364 Exactly like C<perform>, but additionally call C<release_actions>
1365 afterwards.
1366
1367 =cut
1368
1369 sub perform {
1370 my ($self, $wd) = @_;
1371
1372 ($wd //= $self->{wd})->perform_actions ($self)
1373 }
1374
1375 sub perform_release {
1376 my ($self, $wd) = @_;
1377
1378 ($wd //= $self->{wd})->perform_actions ($self);
1379 $wd->release_actions;
1380 }
1381
1382 =item ($actions, $duration) = $al->compile
1383
1384 Finalises and compiles the list, if not done yet, and returns an actions
1385 object suitable for calls to C<< $wd->perform_actions >>. When called in
1386 list context, additionally returns the total duration of the action list.
1387
1388 Since building large action lists can take nontrivial amounts of time,
1389 it can make sense to build an action list only once and then perform it
1390 multiple times.
1391
1392 Actions must not be added after compiling a list.
1393
1394 =cut
1395
1396 sub compile {
1397 my ($self) = @_;
1398
1399 $self->{duration} += delete $self->{tick_duration};
1400
1401 delete $self->{tick};
1402 delete $self->{last_kbd};
1403 delete $self->{last_ptr};
1404
1405 $self->{actions} ||= [values %{ delete $self->{source} }];
1406
1407 wantarray
1408 ? ($self->{actions}, $self->{duration})
1409 : $self->{actions}
1410 }
1411
1412 =back
1413
1414 =head2 EVENT BASED API
1415
1416 This module wouldn't be a good AnyEvent citizen if it didn't have a true
1417 event-based API.
1418
1419 In fact, the simplified API, as documented above, is emulated via the
1420 event-based API and an C<AUTOLOAD> function that automatically provides
1421 blocking wrappers around the callback-based API.
1422
1423 Every method documented in the L<SIMPLIFIED API> section has an equivalent
1424 event-based method that is formed by appending a underscore (C<_>) to the
1425 method name, and appending a callback to the argument list (mnemonic: the
1426 underscore indicates the "the action is not yet finished" after the call
1427 returns).
1428
1429 For example, instead of a blocking calls to C<new_session>, C<navigate_to>
1430 and C<back>, you can make a callback-based ones:
1431
1432 my $cv = AE::cv;
1433
1434 $wd->new_session ({}, sub {
1435 my ($status, $value) = @_,
1436
1437 die "error $value->{error}" if $status ne "200";
1438
1439 $wd->navigate_to_ ("http://www.nethype.de", sub {
1440
1441 $wd->back_ (sub {
1442 print "all done\n";
1443 $cv->send;
1444 });
1445
1446 });
1447 });
1448
1449 $cv->recv;
1450
1451 While the blocking methods C<croak> on errors, the callback-based ones all
1452 pass two values to the callback, C<$status> and C<$res>, where C<$status>
1453 is the HTTP status code (200 for successful requests, typically 4xx or
1454 5xx for errors), and C<$res> is the value of the C<value> key in the JSON
1455 response object.
1456
1457 Other than that, the underscore variants and the blocking variants are
1458 identical.
1459
1460 =head2 LOW LEVEL API
1461
1462 All the simplified API methods are very thin wrappers around WebDriver
1463 commands of the same name. They are all implemented in terms of the
1464 low-level methods (C<req>, C<get>, C<post> and C<delete>), which exists
1465 in blocking and callback-based variants (C<req_>, C<get_>, C<post_> and
1466 C<delete_>).
1467
1468 Examples are after the function descriptions.
1469
1470 =over
1471
1472 =item $wd->req_ ($method, $uri, $body, $cb->($status, $value))
1473
1474 =item $value = $wd->req ($method, $uri, $body)
1475
1476 Appends the C<$uri> to the C<endpoint/session/{sessionid}/> URL and makes
1477 a HTTP C<$method> request (C<GET>, C<POST> etc.). C<POST> requests can
1478 provide a UTF-8-encoded JSON text as HTTP request body, or the empty
1479 string to indicate no body is used.
1480
1481 For the callback version, the callback gets passed the HTTP status code
1482 (200 for every successful request), and the value of the C<value> key in
1483 the JSON response object as second argument.
1484
1485 =item $wd->get_ ($uri, $cb->($status, $value))
1486
1487 =item $value = $wd->get ($uri)
1488
1489 Simply a call to C<req_> with C<$method> set to C<GET> and an empty body.
1490
1491 =item $wd->post_ ($uri, $data, $cb->($status, $value))
1492
1493 =item $value = $wd->post ($uri, $data)
1494
1495 Simply a call to C<req_> with C<$method> set to C<POST> - if C<$body> is
1496 C<undef>, then an empty object is send, otherwise, C<$data> must be a
1497 valid request object, which gets encoded into JSON for you.
1498
1499 =item $wd->delete_ ($uri, $cb->($status, $value))
1500
1501 =item $value = $wd->delete ($uri)
1502
1503 Simply a call to C<req_> with C<$method> set to C<DELETE> and an empty body.
1504
1505 =cut
1506
1507 =back
1508
1509 Example: implement C<get_all_cookies>, which is a simple C<GET> request
1510 without any parameters:
1511
1512 $cookies = $wd->get ("cookie");
1513
1514 Example: implement C<execute_script>, which needs some parameters:
1515
1516 $results = $wd->post ("execute/sync" => { script => "$javascript", args => [] });
1517
1518 Example: call C<find_elements> to find all C<IMG> elements:
1519
1520 $elems = $wd->post (elements => { using => "css selector", value => "img" });
1521
1522 =cut
1523
1524 =head1 HISTORY
1525
1526 This module was unintentionally created (it started inside some quickly
1527 hacked-together script) simply because I couldn't get the existing
1528 C<Selenium::Remote::Driver> module to work, ever, despite multiple
1529 attempts over the years and trying to report multiple bugs, which have
1530 been completely ignored. It's also not event-based, so, yeah...
1531
1532 =head1 AUTHOR
1533
1534 Marc Lehmann <schmorp@schmorp.de>
1535 http://anyevent.schmorp.de
1536
1537 =cut
1538
1539 1
1540