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