ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-WebDriver/WebDriver.pm
Revision: 1.52
Committed: Fri Mar 27 17:50:16 2020 UTC (4 years, 3 months ago) by root
Branch: MAIN
Changes since 1.51: +142 -82 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(chromedriver or any other webdriver via the shell
8 $ geckodriver -b myfirefox/firefox --log trace --port 4444
9 # chromedriver --port=4444
10
11 # then use it
12 use AnyEvent::WebDriver;
13
14 # create a new webdriver object
15 my $wd = new AnyEvent::WebDriver;
16
17 # create a new session with default capabilities.
18 $wd->new_session ({});
19
20 $wd->navigate_to ("https://duckduckgo.com/html");
21 my $searchbox = $wd->find_element (css => 'input[type="text"]');
22
23 $wd->element_send_keys ($searchbox => "free software");
24 $wd->element_click ($wd->find_element (css => 'input[type="submit"]'));
25
26 # session gets autodeleted by default, so wait a bit
27 sleep 10;
28
29 # this is an example of an action sequence
30 $wd->actions
31 ->move ($wd->find_element (...), 40, 5)
32 ->click
33 ->type ("some text")
34 ->key ("{Enter}")
35 ->perform;
36
37 =head1 DESCRIPTION
38
39 This module aims to implement the L<W3C
40 WebDriver|https://www.w3.org/TR/webdriver1/> specification which is the
41 standardised equivalent to the Selenium WebDriver API, which in turn aims
42 at remotely controlling web browsers such as Firefox or Chromium.
43
44 One of the design goals of this module was to stay very close to the
45 language and words used in the WebDriver specification itself, so to make
46 most of this module, or, in fact, to make any reasonable use of this
47 module, you would need to refer to the W3C WebDriver recommendation, which
48 can be found L<here|https://www.w3.org/TR/webdriver1/>:
49
50 https://www.w3.org/TR/webdriver1/
51
52 Mozilla's C<geckodriver> has had webdriver support for a long time, while
53 C<chromedriver> only has basic and mostly undocumented webdriver support
54 as of release 77.
55
56 =head2 CONVENTIONS
57
58 Unless otherwise stated, all delays and time differences in this module
59 are represented as an integer number of milliseconds.
60
61 =cut
62
63 package AnyEvent::WebDriver;
64
65 use common::sense;
66
67 use Carp ();
68 use AnyEvent ();
69 use AnyEvent::HTTP ();
70
71 our $VERSION = '1.01';
72
73 our $WEB_ELEMENT_IDENTIFIER = "element-6066-11e4-a52e-4f735466cecf";
74 our $WEB_WINDOW_IDENTIFIER = "window-fcc6-11e5-b4f8-330a88ab9d7f";
75 our $WEB_FRAME_IDENTIFIER = "frame-075b-4da1-b6ba-e579c2d3230a";
76
77 my $json = eval { require JSON::XS; JSON::XS:: } || do { require JSON::PP; JSON::PP:: };
78 $json = $json->new->utf8;
79
80 $json->boolean_values (0, 1)
81 if $json->can ("boolean_values");
82
83 sub _decode_base64 {
84 require MIME::Base64;
85
86 MIME::Base64::decode_base64 (shift)
87 }
88
89 sub req_ {
90 my ($self, $method, $ep, $body, $cb) = @_;
91
92 AnyEvent::HTTP::http_request $method => "$self->{_ep}$ep",
93 body => $body,
94 $self->{persistent} ? (persistent => 1) : (),
95 $self->{proxy} eq "default" ? () : (proxy => $self->{proxy}),
96 timeout => $self->{timeout},
97 headers => { "content-type" => "application/json; charset=utf-8", "cache-control" => "no-cache" },
98 sub {
99 my ($res, $hdr) = @_;
100
101 $res = eval { $json->decode ($res) };
102 $hdr->{Status} = 500 unless exists $res->{value};
103
104 $cb->($hdr->{Status}, $res->{value});
105 }
106 ;
107 }
108
109 sub get_ {
110 my ($self, $ep, $cb) = @_;
111
112 $self->req_ (GET => $ep, undef, $cb)
113 }
114
115 sub post_ {
116 my ($self, $ep, $data, $cb) = @_;
117
118 $self->req_ (POST => $ep, $json->encode ($data || {}), $cb)
119 }
120
121 sub delete_ {
122 my ($self, $ep, $cb) = @_;
123
124 $self->req_ (DELETE => $ep, "", $cb)
125 }
126
127 sub AUTOLOAD {
128 our $AUTOLOAD;
129
130 $_[0]->isa (__PACKAGE__)
131 or Carp::croak "$AUTOLOAD: no such function";
132
133 (my $name = $AUTOLOAD) =~ s/^.*://;
134
135 my $name_ = "$name\_";
136
137 defined &$name_
138 or Carp::croak "$AUTOLOAD: no such method";
139
140 my $func_ = \&$name_;
141
142 *$name = sub {
143 $func_->(@_, my $cv = AE::cv);
144 my ($status, $res) = $cv->recv;
145
146 if ($status ne "200") {
147 my $msg;
148
149 if (exists $res->{error}) {
150 $msg = "AyEvent::WebDriver: $res->{error}: $res->{message}";
151 $msg .= "\n$res->{stacktrace}caught at" if length $res->{stacktrace};
152 } else {
153 $msg = "AnyEvent::WebDriver: http status $status (wrong endpoint?), caught";
154 }
155
156 Carp::croak $msg;
157 }
158
159 $res
160 };
161
162 goto &$name;
163 }
164
165 =head2 WEBDRIVER OBJECTS
166
167 =over
168
169 =item new AnyEvent::WebDriver key => value...
170
171 Create a new WebDriver object. Example for a remote WebDriver connection
172 (the only type supported at the moment):
173
174 my $wd = new AnyEvent::WebDriver endpoint => "http://localhost:4444";
175
176 Supported keys are:
177
178 =over
179
180 =item endpoint => $string
181
182 For remote connections, the endpoint to connect to (defaults to C<http://localhost:4444>).
183
184 =item proxy => $proxyspec
185
186 The proxy to use (same as the C<proxy> argument used by
187 L<AnyEvent::HTTP>). The default is C<undef>, which disables proxies. To
188 use the system-provided proxy (e.g. C<http_proxy> environment variable),
189 specify the string C<default>.
190
191 =item autodelete => $boolean
192
193 If true (the default), then automatically execute C<delete_session> when
194 the WebDriver object is destroyed with an active session. If set to a
195 false value, then the session will continue to exist.
196
197 Note that due to bugs in perl that are unlikely to get fixed,
198 C<autodelete> is likely ineffective during global destruction and might
199 even crash your process, so you should ensure objects go out of scope
200 before that, or explicitly call C<delete_session>, if you want the session
201 to be cleaned up.
202
203 =item timeout => $seconds
204
205 The HTTP timeout, in (fractional) seconds (default: C<300>). This timeout
206 is reset on any activity, so it is not an overall request timeout. Also,
207 individual requests might extend this timeout if they are known to take
208 longer.
209
210 =item persistent => C<1> | C<undef>
211
212 If true (the default) then persistent connections will be used for all
213 requests, which assumes you have a reasonably stable connection (such as
214 to C<localhost> :) and that the WebDriver has a persistent timeout much
215 higher than what L<AnyEvent::HTTP> uses.
216
217 You can force connections to be closed for non-idempotent requests (the
218 safe default of L<AnyEvent::HTTP>) by setting this to C<undef>.
219
220 =back
221
222 =cut
223
224 sub new {
225 my ($class, %kv) = @_;
226
227 bless {
228 endpoint => "http://localhost:4444",
229 proxy => undef,
230 persistent => 1,
231 autodelete => 1,
232 timeout => 300,
233 %kv,
234 }, $class
235 }
236
237 sub DESTROY {
238 my ($self) = @_;
239
240 $self->delete_session
241 if exists $self->{sid} && $self->{autodelete};
242 }
243
244 =item $al = $wd->actions
245
246 Creates an action list associated with this WebDriver. See L<ACTION
247 LISTS>, below, for full details.
248
249 =cut
250
251 sub actions {
252 AnyEvent::WebDriver::Actions->new (wd => $_[0])
253 }
254
255 =item $sessionstring = $wd->save_session
256
257 Save the current session in a string so it can be restored load with
258 C<load_session>. Note that only the session data itself is stored
259 (currently the session id and capabilities), not the endpoint information
260 itself.
261
262 The main use of this function is in conjunction with disabled
263 C<autodelete>, to save a session to e.g., and restore it later. It could
264 presumably used for other applications, such as using the same session
265 from multiple processes and so on.
266
267 =item $wd->load_session ($sessionstring)
268
269 =item $wd->set_session ($sessionid, $capabilities)
270
271 Starts using the given session, as identified by
272 C<$sessionid>. C<$capabilities> should be the original session
273 capabilities, although the current version of this module does not make
274 any use of it.
275
276 The C<$sessionid> is stored in C<< $wd->{sid} >> (and could be fetched
277 form there for later use), while the capabilities are stored in C<<
278 $wd->{capabilities} >>.
279
280 =cut
281
282 sub save_session {
283 my ($self) = @_;
284
285 $json->encode ([1, $self->{sid}, $self->{capabilities}]);
286 }
287
288 sub load_session {
289 my ($self, $session) = @_;
290
291 $session = $json->decode ($session);
292
293 $session->[0] == 1
294 or Carp::croak "AnyEvent::WebDriver::load_session: session corrupted or from different version";
295
296 $self->set_session ($session->[1], $session->[2]);
297 }
298
299 sub set_session {
300 my ($self, $sid, $caps) = @_;
301
302 $self->{sid} = $sid;
303 $self->{capabilities} = $caps;
304
305 $self->{_ep} = "$self->{endpoint}/session/$self->{sid}/";
306 }
307
308 =back
309
310 =head2 SIMPLIFIED API
311
312 This section documents the simplified API, which is really just a very
313 thin wrapper around the WebDriver protocol commands. They all block the
314 caller until the result is available (using L<AnyEvent> condvars), so must
315 not be called from an event loop callback - see L<EVENT BASED API> for an
316 alternative.
317
318 The method names are pretty much taken directly from the W3C WebDriver
319 specification, e.g. the request documented in the "Get All Cookies"
320 section is implemented via the C<get_all_cookies> method.
321
322 The order is the same as in the WebDriver draft at the time of this
323 writing, and only minimal massaging is done to request parameters and
324 results.
325
326 =head3 SESSIONS
327
328 =over
329
330 =cut
331
332 =item $wd->new_session ({ key => value... })
333
334 Try to connect to the WebDriver and initialize a new session with a
335 "new session" command, passing the given key-value pairs as value
336 (e.g. C<capabilities>).
337
338 No session-dependent methods must be called before this function returns
339 successfully, and only one session can be created per WebDriver object.
340
341 On success, C<< $wd->{sid} >> is set to the session ID, and C<<
342 $wd->{capabilities} >> is set to the returned capabilities.
343
344 Simple example of creating a WebDriver object and a new session:
345
346 my $wd = new AnyEvent::WebDriver endpoint => "http://localhost:4444";
347 $wd->new_session ({});
348
349 Real-world example with capability negotiation:
350
351 $wd->new_session ({
352 capabilities => {
353 alwaysMatch => {
354 pageLoadStrategy => "eager",
355 unhandledPromptBehavior => "dismiss",
356 # proxy => { proxyType => "manual", httpProxy => "1.2.3.4:56", sslProxy => "1.2.3.4:56" },
357 },
358 firstMatch => [
359 {
360 browserName => "firefox",
361 "moz:firefoxOptions" => {
362 binary => "firefox/firefox",
363 args => ["-devtools", "-headless"],
364 prefs => {
365 "dom.webnotifications.enabled" => \0,
366 "dom.push.enabled" => \0,
367 "dom.disable_beforeunload" => \1,
368 "browser.link.open_newwindow" => 3,
369 "browser.link.open_newwindow.restrictions" => 0,
370 "dom.popup_allowed_events" => "",
371 "dom.disable_open_during_load" => \1,
372 },
373 },
374 },
375 {
376 browserName => "chrome",
377 "goog:chromeOptions" => {
378 binary => "/bin/chromium",
379 args => ["--no-sandbox", "--headless"],
380 prefs => {
381 # ...
382 },
383 },
384 },
385 {
386 # generic fallback
387 },
388 ],
389
390 },
391 });
392
393 Firefox-specific capability documentation can be found L<on
394 MDN|https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities>,
395 Chrome-specific capability documentation might be found
396 L<here|http://chromedriver.chromium.org/capabilities>, but the latest
397 release at the time of this writing (chromedriver 77) has essentially
398 no documentation about webdriver capabilities (even MDN has better
399 documentation about chromwedriver!)
400
401 If you have URLs for Safari/IE/Edge etc. capabilities, feel free to tell
402 me about them.
403
404 =cut
405
406 sub new_session_ {
407 my ($self, $kv, $cb) = @_;
408
409 $kv->{capabilities} ||= {}; # required by protocol
410
411 local $self->{_ep} = "$self->{endpoint}/";
412 $self->post_ (session => $kv, sub {
413 my ($status, $res) = @_;
414
415 exists $res->{capabilities}
416 or $status = "500"; # blasted chromedriver
417
418 $self->set_session ($res->{sessionId}, $res->{capabilities})
419 if $status eq "200";
420
421 $cb->($status, $res);
422 });
423 }
424
425 =item $wd->delete_session
426
427 Deletes the session - the WebDriver object must not be used after this
428 call (except for calling this method).
429
430 This method is always safe to call and will not do anything if there is no
431 active session.
432
433 =cut
434
435 sub delete_session_ {
436 my ($self, $cb) = @_;
437
438 my $sid = delete $self->{sid};
439 delete $self->{capoabilities};
440
441 return unless defined $sid;
442
443 local $self->{_ep} = "$self->{endpoint}/session/$sid";
444 $self->delete_ ("" => $cb);
445 }
446
447 =item $timeouts = $wd->get_timeouts
448
449 Get the current timeouts, e.g.:
450
451 my $timeouts = $wd->get_timeouts;
452 => { implicit => 0, pageLoad => 300000, script => 30000 }
453
454 =item $wd->set_timeouts ($timeouts)
455
456 Sets one or more timeouts, e.g.:
457
458 $wd->set_timeouts ({ script => 60000 });
459
460 =cut
461
462 sub get_timeouts_ {
463 $_[0]->get_ (timeouts => $_[1], $_[2]);
464 }
465
466 sub set_timeouts_ {
467 $_[0]->post_ (timeouts => $_[1], $_[2], $_[3]);
468 }
469
470 =back
471
472 =head3 NAVIGATION
473
474 =over
475
476 =cut
477
478 =item $wd->navigate_to ($url)
479
480 Navigates to the specified URL.
481
482 =item $url = $wd->get_current_url
483
484 Queries the current page URL as set by C<navigate_to>.
485
486 =cut
487
488 sub navigate_to_ {
489 $_[0]->post_ (url => { url => "$_[1]" }, $_[2]);
490 }
491
492 sub get_current_url_ {
493 $_[0]->get_ (url => $_[1])
494 }
495
496 =item $wd->back
497
498 The equivalent of pressing "back" in the browser.
499
500 =item $wd->forward
501
502 The equivalent of pressing "forward" in the browser.
503
504 =item $wd->refresh
505
506 The equivalent of pressing "refresh" in the browser.
507
508 =cut
509
510 sub back_ {
511 $_[0]->post_ (back => undef, $_[1]);
512 }
513
514 sub forward_ {
515 $_[0]->post_ (forward => undef, $_[1]);
516 }
517
518 sub refresh_ {
519 $_[0]->post_ (refresh => undef, $_[1]);
520 }
521
522 =item $title = $wd->get_title
523
524 Returns the current document title.
525
526 =cut
527
528 sub get_title_ {
529 $_[0]->get_ (title => $_[1]);
530 }
531
532 =back
533
534 =head3 COMMAND CONTEXTS
535
536 =over
537
538 =cut
539
540 =item $handle = $wd->get_window_handle
541
542 Returns the current window handle.
543
544 =item $wd->close_window
545
546 Closes the current browsing context.
547
548 =item $wd->switch_to_window ($handle)
549
550 Changes the current browsing context to the given window.
551
552 =cut
553
554 sub get_window_handle_ {
555 $_[0]->get_ (window => $_[1]);
556 }
557
558 sub close_window_ {
559 $_[0]->delete_ (window => $_[1]);
560 }
561
562 sub switch_to_window_ {
563 $_[0]->post_ (window => { handle => "$_[1]" }, $_[2]);
564 }
565
566 =item $handles = $wd->get_window_handles
567
568 Return the current window handles as an array-ref of handle IDs.
569
570 =cut
571
572 sub get_window_handles_ {
573 $_[0]->get_ ("window/handles" => $_[1]);
574 }
575
576 =item $handles = $wd->switch_to_frame ($frame)
577
578 Switch to the given frame identified by C<$frame>, which must be either
579 C<undef> to go back to the top-level browsing context, an integer to
580 select the nth subframe, or an element object.
581
582 =cut
583
584 sub switch_to_frame_ {
585 $_[0]->post_ (frame => { id => "$_[1]" }, $_[2]);
586 }
587
588 =item $handles = $wd->switch_to_parent_frame
589
590 Switch to the parent frame.
591
592 =cut
593
594 sub switch_to_parent_frame_ {
595 $_[0]->post_ ("frame/parent" => undef, $_[1]);
596 }
597
598 =item $rect = $wd->get_window_rect
599
600 Return the current window rect(angle), e.g.:
601
602 $rect = $wd->get_window_rect
603 => { height => 1040, width => 540, x => 0, y => 0 }
604
605 =item $wd->set_window_rect ($rect)
606
607 Sets the window rect(angle), e.g.:
608
609 $wd->set_window_rect ({ width => 780, height => 560 });
610 $wd->set_window_rect ({ x => 0, y => 0, width => 780, height => 560 });
611
612 =cut
613
614 sub get_window_rect_ {
615 $_[0]->get_ ("window/rect" => $_[1]);
616 }
617
618 sub set_window_rect_ {
619 $_[0]->post_ ("window/rect" => $_[1], $_[2]);
620 }
621
622 =item $wd->maximize_window
623
624 =item $wd->minimize_window
625
626 =item $wd->fullscreen_window
627
628 Changes the window size by either maximising, minimising or making it
629 fullscreen. In my experience, this will timeout if no window manager is
630 running.
631
632 =cut
633
634 sub maximize_window_ {
635 $_[0]->post_ ("window/maximize" => undef, $_[1]);
636 }
637
638 sub minimize_window_ {
639 $_[0]->post_ ("window/minimize" => undef, $_[1]);
640 }
641
642 sub fullscreen_window_ {
643 $_[0]->post_ ("window/fullscreen" => undef, $_[1]);
644 }
645
646 =back
647
648 =head3 ELEMENT RETRIEVAL
649
650 To reduce typing and memory strain, the element finding functions accept
651 some shorter and hopefully easier to remember aliases for the standard
652 locator strategy values, as follows:
653
654 Alias Locator Strategy
655 css css selector
656 link link text
657 substr partial link text
658 tag tag name
659
660 =over
661
662 =cut
663
664 our %USING = (
665 css => "css selector",
666 link => "link text",
667 substr => "partial link text",
668 tag => "tag name",
669 );
670
671 sub _using($) {
672 using => $USING{$_[0]} // "$_[0]"
673 }
674
675 =item $element = $wd->find_element ($locator_strategy, $selector)
676
677 Finds the first element specified by the given selector and returns its
678 element object. Raises an error when no element was found.
679
680 Examples showing all standard locator strategies:
681
682 $element = $wd->find_element ("css selector" => "body a");
683 $element = $wd->find_element ("link text" => "Click Here For Porn");
684 $element = $wd->find_element ("partial link text" => "orn");
685 $element = $wd->find_element ("tag name" => "input");
686 $element = $wd->find_element ("xpath" => '//input[@type="text"]');
687 => e.g. { "element-6066-11e4-a52e-4f735466cecf" => "decddca8-5986-4e1d-8c93-efe952505a5f" }
688
689 Same examples using aliases provided by this module:
690
691 $element = $wd->find_element (css => "body a");
692 $element = $wd->find_element (link => "Click Here For Porn");
693 $element = $wd->find_element (substr => "orn");
694 $element = $wd->find_element (tag => "input");
695
696 =item $elements = $wd->find_elements ($locator_strategy, $selector)
697
698 As above, but returns an arrayref of all found element objects.
699
700 =item $element = $wd->find_element_from_element ($element, $locator_strategy, $selector)
701
702 Like C<find_element>, but looks only inside the specified C<$element>.
703
704 =item $elements = $wd->find_elements_from_element ($element, $locator_strategy, $selector)
705
706 Like C<find_elements>, but looks only inside the specified C<$element>.
707
708 my $head = $wd->find_element ("tag name" => "head");
709 my $links = $wd->find_elements_from_element ($head, "tag name", "link");
710
711 =item $element = $wd->get_active_element
712
713 Returns the active element.
714
715 =cut
716
717 sub find_element_ {
718 $_[0]->post_ (element => { _using $_[1], value => "$_[2]" }, $_[3]);
719 }
720
721 sub find_elements_ {
722 $_[0]->post_ (elements => { _using $_[1], value => "$_[2]" }, $_[3]);
723 }
724
725 sub find_element_from_element_ {
726 $_[0]->post_ ("element/$_[1]/element" => { _using $_[2], value => "$_[3]" }, $_[4]);
727 }
728
729 sub find_elements_from_element_ {
730 $_[0]->post_ ("element/$_[1]/elements" => { _using $_[2], value => "$_[3]" }, $_[4]);
731 }
732
733 sub get_active_element_ {
734 $_[0]->get_ ("element/active" => $_[1]);
735 }
736
737 =back
738
739 =head3 ELEMENT STATE
740
741 =over
742
743 =cut
744
745 =item $bool = $wd->is_element_selected
746
747 Returns whether the given input or option element is selected or not.
748
749 =item $string = $wd->get_element_attribute ($element, $name)
750
751 Returns the value of the given attribute.
752
753 =item $string = $wd->get_element_property ($element, $name)
754
755 Returns the value of the given property.
756
757 =item $string = $wd->get_element_css_value ($element, $name)
758
759 Returns the value of the given CSS value.
760
761 =item $string = $wd->get_element_text ($element)
762
763 Returns the (rendered) text content of the given element.
764
765 =item $string = $wd->get_element_tag_name ($element)
766
767 Returns the tag of the given element.
768
769 =item $rect = $wd->get_element_rect ($element)
770
771 Returns the element rect(angle) of the given element.
772
773 =item $bool = $wd->is_element_enabled
774
775 Returns whether the element is enabled or not.
776
777 =cut
778
779 sub is_element_selected_ {
780 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/selected" => $_[2]);
781 }
782
783 sub get_element_attribute_ {
784 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/attribute/$_[2]" => $_[3]);
785 }
786
787 sub get_element_property_ {
788 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/property/$_[2]" => $_[3]);
789 }
790
791 sub get_element_css_value_ {
792 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/css/$_[2]" => $_[3]);
793 }
794
795 sub get_element_text_ {
796 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/text" => $_[2]);
797 }
798
799 sub get_element_tag_name_ {
800 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/name" => $_[2]);
801 }
802
803 sub get_element_rect_ {
804 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/rect" => $_[2]);
805 }
806
807 sub is_element_enabled_ {
808 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/enabled" => $_[2]);
809 }
810
811 =back
812
813 =head3 ELEMENT INTERACTION
814
815 =over
816
817 =cut
818
819 =item $wd->element_click ($element)
820
821 Clicks the given element.
822
823 =item $wd->element_clear ($element)
824
825 Clear the contents of the given element.
826
827 =item $wd->element_send_keys ($element, $text)
828
829 Sends the given text as key events to the given element. Key input state
830 can be cleared by embedding C<\x{e000}> in C<$text>. Presumably, you can
831 embed modifiers using their unicode codepoints, but the specification is
832 less than clear to mein this area.
833
834 =cut
835
836 sub element_click_ {
837 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/click" => undef, $_[2]);
838 }
839
840 sub element_clear_ {
841 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/clear" => undef, $_[2]);
842 }
843
844 sub element_send_keys_ {
845 $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/value" => { text => "$_[2]" }, $_[3]);
846 }
847
848 =back
849
850 =head3 DOCUMENT HANDLING
851
852 =over
853
854 =cut
855
856 =item $source = $wd->get_page_source
857
858 Returns the (HTML/XML) page source of the current document.
859
860 =item $results = $wd->execute_script ($javascript, $args)
861
862 Synchronously execute the given script with given arguments and return its
863 results (C<$args> can be C<undef> if no arguments are wanted/needed).
864
865 $ten = $wd->execute_script ("return arguments[0]+arguments[1]", [3, 7]);
866
867 =item $results = $wd->execute_async_script ($javascript, $args)
868
869 Similar to C<execute_script>, but doesn't wait for script to return, but
870 instead waits for the script to call its last argument, which is added to
871 C<$args> automatically.
872
873 $twenty = $wd->execute_async_script ("arguments[0](20)", undef);
874
875 =cut
876
877 sub get_page_source_ {
878 $_[0]->get_ (source => $_[1]);
879 }
880
881 sub execute_script_ {
882 $_[0]->post_ ("execute/sync" => { script => "$_[1]", args => $_[2] || [] }, $_[3]);
883 }
884
885 sub execute_async_script_ {
886 $_[0]->post_ ("execute/async" => { script => "$_[1]", args => $_[2] || [] }, $_[3]);
887 }
888
889 =back
890
891 =head3 COOKIES
892
893 =over
894
895 =cut
896
897 =item $cookies = $wd->get_all_cookies
898
899 Returns all cookies, as an arrayref of hashrefs.
900
901 # google surely sets a lot of cookies without my consent
902 $wd->navigate_to ("http://google.com");
903 use Data::Dump;
904 ddx $wd->get_all_cookies;
905
906 =item $cookie = $wd->get_named_cookie ($name)
907
908 Returns a single cookie as a hashref.
909
910 =item $wd->add_cookie ($cookie)
911
912 Adds the given cookie hashref.
913
914 =item $wd->delete_cookie ($name)
915
916 Delete the named cookie.
917
918 =item $wd->delete_all_cookies
919
920 Delete all cookies.
921
922 =cut
923
924 sub get_all_cookies_ {
925 $_[0]->get_ (cookie => $_[1]);
926 }
927
928 sub get_named_cookie_ {
929 $_[0]->get_ ("cookie/$_[1]" => $_[2]);
930 }
931
932 sub add_cookie_ {
933 $_[0]->post_ (cookie => { cookie => $_[1] }, $_[2]);
934 }
935
936 sub delete_cookie_ {
937 $_[0]->delete_ ("cookie/$_[1]" => $_[2]);
938 }
939
940 sub delete_all_cookies_ {
941 $_[0]->delete_ (cookie => $_[2]);
942 }
943
944 =back
945
946 =head3 ACTIONS
947
948 =over
949
950 =cut
951
952 =item $wd->perform_actions ($actions)
953
954 Perform the given actions (an arrayref of action specifications simulating
955 user activity, or an C<AnyEvent::WebDriver::Actions> object). For further
956 details, read the spec or the section L<ACTION LISTS>, below.
957
958 An example to get you started (see the next example for a mostly
959 equivalent example using the C<AnyEvent::WebDriver::Actions> helper API):
960
961 $wd->navigate_to ("https://duckduckgo.com/html");
962 my $input = $wd->find_element ("css selector", 'input[type="text"]');
963 $wd->perform_actions ([
964 {
965 id => "myfatfinger",
966 type => "pointer",
967 pointerType => "touch",
968 actions => [
969 { type => "pointerMove", duration => 100, origin => $input, x => 40, y => 5 },
970 { type => "pointerDown", button => 0 },
971 { type => "pause", duration => 40 },
972 { type => "pointerUp", button => 0 },
973 ],
974 },
975 {
976 id => "mykeyboard",
977 type => "key",
978 actions => [
979 { type => "pause" },
980 { type => "pause" },
981 { type => "pause" },
982 { type => "pause" },
983 { type => "keyDown", value => "a" },
984 { type => "pause", duration => 100 },
985 { type => "keyUp", value => "a" },
986 { type => "pause", duration => 100 },
987 { type => "keyDown", value => "b" },
988 { type => "pause", duration => 100 },
989 { type => "keyUp", value => "b" },
990 { type => "pause", duration => 2000 },
991 { type => "keyDown", value => "\x{E007}" }, # enter
992 { type => "pause", duration => 100 },
993 { type => "keyUp", value => "\x{E007}" }, # enter
994 { type => "pause", duration => 5000 },
995 ],
996 },
997 ]);
998
999 And here is essentially the same (except for fewer pauses) example as
1000 above, using the much simpler C<AnyEvent::WebDriver::Actions> API:
1001
1002 $wd->navigate_to ("https://duckduckgo.com/html");
1003 my $input = $wd->find_element ("css selector", 'input[type="text"]');
1004 $wd->actions
1005 ->move ($input, 40, 5, "touch1")
1006 ->click
1007 ->key ("a")
1008 ->key ("b")
1009 ->pause (2000) # so you can watch leisurely
1010 ->key ("{Enter}")
1011 ->pause (5000) # so you can see the result
1012 ->perform;
1013
1014 =item $wd->release_actions
1015
1016 Release all keys and pointer buttons currently depressed.
1017
1018 =cut
1019
1020 sub perform_actions_ {
1021 if (UNIVERSAL::isa $_[1], AnyEvent::WebDriver::Actions::) {
1022 my ($actions, $duration) = $_[1]->compile;
1023 local $_[0]{timeout} = $_[0]{timeout} + $duration * 1e-3;
1024 $_[0]->post_ (actions => { actions => $actions }, $_[2]);
1025 } else {
1026 $_[0]->post_ (actions => { actions => $_[1] }, $_[2]);
1027 }
1028 }
1029
1030 sub release_actions_ {
1031 $_[0]->delete_ (actions => $_[1]);
1032 }
1033
1034 =back
1035
1036 =head3 USER PROMPTS
1037
1038 =over
1039
1040 =cut
1041
1042 =item $wd->dismiss_alert
1043
1044 Dismiss a simple dialog, if present.
1045
1046 =item $wd->accept_alert
1047
1048 Accept a simple dialog, if present.
1049
1050 =item $text = $wd->get_alert_text
1051
1052 Returns the text of any simple dialog.
1053
1054 =item $text = $wd->send_alert_text
1055
1056 Fills in the user prompt with the given text.
1057
1058
1059 =cut
1060
1061 sub dismiss_alert_ {
1062 $_[0]->post_ ("alert/dismiss" => undef, $_[1]);
1063 }
1064
1065 sub accept_alert_ {
1066 $_[0]->post_ ("alert/accept" => undef, $_[1]);
1067 }
1068
1069 sub get_alert_text_ {
1070 $_[0]->get_ ("alert/text" => $_[1]);
1071 }
1072
1073 sub send_alert_text_ {
1074 $_[0]->post_ ("alert/text" => { text => "$_[1]" }, $_[2]);
1075 }
1076
1077 =back
1078
1079 =head3 SCREEN CAPTURE
1080
1081 =over
1082
1083 =cut
1084
1085 =item $wd->take_screenshot
1086
1087 Create a screenshot, returning it as a PNG image. To decode and save, you
1088 coulöd do something like:
1089
1090 use MIME::Base64 ();
1091
1092 my $screenshot = $wd->take_screenshot;
1093
1094 open my $fh, ">", "screenshot.png" or die "screenshot.png: $!\n";
1095
1096 syswrite $fh, MIME::Base64::decode_base64 $screenshot;
1097
1098 =item $wd->take_element_screenshot ($element)
1099
1100 Similar to C<take_screenshot>, but only takes a screenshot of the bounding
1101 box of a single element.
1102
1103 Note: The W3C WebDriver specification also mentions a "scroll" parameter,
1104 but it is impossible to pass parameters for this command, so it is
1105 czrrently left out.
1106
1107 =cut
1108
1109 sub take_screenshot_ {
1110 my $cb = pop; push @_, sub { $cb->($_[0], _decode_base64 $_[1]) };
1111 $_[0]->get_ (screenshot => $_[1]);
1112 }
1113
1114 sub take_element_screenshot_ {
1115 my $cb = pop; push @_, sub { $cb->($_[0], _decode_base64 $_[1]) };
1116 $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/screenshot" => $_[2]);
1117 }
1118
1119 =back
1120
1121 =head3 PRINT
1122
1123 =over
1124
1125 =cut
1126
1127 =item $wd->print_page (key => value...)
1128
1129 Create a printed version of the document, returning it as a PDF document
1130 encoded as base64. See C<take_screenshot> for an example on how to decode
1131 and save such a string.
1132
1133 This command takes a lot of optional parameters, see L<the print
1134 section|https://www.w3.org/TR/webdriver2/#print> of the WebDriver
1135 specification for details.
1136
1137 This command is taken from a draft document, so it might change in the
1138 future.
1139
1140 =cut
1141
1142 sub print_page {
1143 my $cb = pop; push @_, sub { $cb->($_[0], _decode_base64 $_[1]) };
1144 $_[0]->post_ (print => { @_ });
1145 }
1146
1147 =head2 ACTION LISTS
1148
1149 Action lists can be quite complicated. Or at least it took a while for
1150 me to twist my head around them. Basically, an action list consists of a
1151 number of sources representing devices (such as a finger, a mouse, a pen
1152 or a keyboard) and a list of actions for each source, in a timeline.
1153
1154 An action can be a key press, a pointer move or a pause (time delay).
1155
1156 While you can provide these action lists manually, it is (hopefully) less
1157 cumbersome to use the API described in this section to create them.
1158
1159 The basic process of creating and performing actions is to create a new
1160 action list, adding action sources, followed by adding actions. Finally
1161 you would C<perform> those actions on the WebDriver.
1162
1163 Most methods here are designed to chain, i.e. they return the web actions
1164 object, to simplify multiple calls.
1165
1166 Also, while actions from different sources can happen "at the same time"
1167 in the WebDriver protocol, this class by default ensures that actions will
1168 execute in the order specified.
1169
1170 For example, to simulate a mouse click to an input element, followed by
1171 entering some text and pressing enter, you can use this:
1172
1173 $wd->actions
1174 ->click (0, 100)
1175 ->type ("some text")
1176 ->key ("{Enter}")
1177 ->perform;
1178
1179 By default, C<keyboard> and C<mouse> input sources are provided and
1180 used. You can create your own sources and use them when adding events. The
1181 above example could be more verbosely written like this:
1182
1183 $wd->actions
1184 ->source ("mouse", "pointer", pointerType => "mouse")
1185 ->source ("kbd", "key")
1186 ->click (0, 100, "mouse")
1187 ->type ("some text", "kbd")
1188 ->key ("{Enter}", "kbd")
1189 ->perform;
1190
1191 When you specify the event source explicitly it will switch the current
1192 "focus" for this class of device (all keyboards are in one class, all
1193 pointer-like devices such as mice/fingers/pens are in one class), so you
1194 don't have to specify the source for subsequent actions that are on the
1195 same class.
1196
1197 When you use the sources C<keyboard>, C<mouse>, C<touch1>..C<touch3>,
1198 C<pen> without defining them, then a suitable default source will be
1199 created for them.
1200
1201 =over 4
1202
1203 =cut
1204
1205 package AnyEvent::WebDriver::Actions;
1206
1207 =item $al = new AnyEvent::WebDriver::Actions
1208
1209 Create a new empty action list object. More often you would use the C<<
1210 $wd->action_list >> method to create one that is already associated with
1211 a given web driver.
1212
1213 =cut
1214
1215 sub new {
1216 my ($class, %kv) = @_;
1217
1218 $kv{last_kbd} = "keyboard";
1219 $kv{last_ptr} = "mouse";
1220
1221 bless \%kv, $class
1222 }
1223
1224 =item $al = $al->source ($id, $type, key => value...)
1225
1226 The first time you call this with a given ID, this defines the event
1227 source using the extra parameters. Subsequent calls merely switch the
1228 current source for its event class.
1229
1230 It's not an error to define built-in sources (such as C<keyboard> or
1231 C<touch1>) differently then the defaults.
1232
1233 Example: define a new touch device called C<fatfinger>.
1234
1235 $al->source (fatfinger => "pointer", pointerType => "touch");
1236
1237 Example: define a new touch device called C<fatfinger>.
1238
1239 $al->source (fatfinger => "pointer", pointerType => "touch");
1240
1241 Example: switch default keyboard source to C<kbd1>, assuming it is of C<key> class.
1242
1243 $al->source ("kbd1");
1244
1245 =cut
1246
1247 sub _default_source($) {
1248 my ($source) = @_;
1249
1250 $source eq "keyboard" ? { actions => [], id => $source, type => "key" }
1251 : $source eq "mouse" ? { actions => [], id => $source, type => "pointer", pointerType => "mouse" }
1252 : $source eq "touch" ? { actions => [], id => $source, type => "pointer", pointerType => "touch" }
1253 : $source eq "pen" ? { actions => [], id => $source, type => "pointer", pointerType => "pen" }
1254 : Carp::croak "AnyEvent::WebDriver::Actions: event source '$source' not defined"
1255 }
1256
1257 my %source_class = (
1258 key => "kbd",
1259 pointer => "ptr",
1260 );
1261
1262 sub source {
1263 my ($self, $id, $type, %kv) = @_;
1264
1265 if (defined $type) {
1266 !exists $self->{source}{$id}
1267 or Carp::croak "AnyEvent::WebDriver::Actions: source '$id' already defined";
1268
1269 $kv{id} = $id;
1270 $kv{type} = $type;
1271 $kv{actions} = [];
1272
1273 $self->{source}{$id} = \%kv;
1274 }
1275
1276 my $source = $self->{source}{$id} ||= _default_source $id;
1277
1278 my $last = $source_class{$source->{type}} // "xxx";
1279
1280 $self->{"last_$last"} = $id;
1281
1282 $self
1283 }
1284
1285 sub _add {
1286 my ($self, $source, $sourcetype, $type, %kv) = @_;
1287
1288 my $last = \$self->{"last_$sourcetype"};
1289
1290 $source
1291 ? ($$last = $source)
1292 : ($source = $$last);
1293
1294 my $source = $self->{source}{$source} ||= _default_source $source;
1295
1296 my $al = $source->{actions};
1297
1298 push @$al, { type => "pause" }
1299 while @$al < $self->{tick}; # -1 == allow concurrent actions
1300
1301 $kv{type} = $type;
1302
1303 push @{ $source->{actions} }, \%kv;
1304
1305 $self->{tick_duration} = $kv{duration}
1306 if $kv{duration} > $self->{tick_duration};
1307
1308 if ($self->{tick} != @$al) {
1309 $self->{tick} = @$al;
1310 $self->{duration} += delete $self->{tick_duration};
1311 }
1312
1313 $self
1314 }
1315
1316 =item $al = $al->pause ($duration)
1317
1318 Creates a pause with the given duration. Makes sure that time progresses
1319 in any case, even when C<$duration> is C<0>.
1320
1321 =cut
1322
1323 sub pause {
1324 my ($self, $duration) = @_;
1325
1326 $self->{tick_duration} = $duration
1327 if $duration > $self->{tick_duration};
1328
1329 $self->{duration} += delete $self->{tick_duration};
1330
1331 # find the source with the longest list
1332
1333 for my $source (values %{ $self->{source} }) {
1334 if (@{ $source->{actions} } == $self->{tick}) {
1335 # this source is one of the longest
1336
1337 # create a pause event only if $duration is non-zero...
1338 push @{ $source->{actions} }, { type => "pause", duration => $duration*1 }
1339 if $duration;
1340
1341 # ... but advance time in any case
1342 ++$self->{tick};
1343
1344 return $self;
1345 }
1346 }
1347
1348 # no event sources are longest. so advance time in any case
1349 ++$self->{tick};
1350
1351 Carp::croak "AnyEvent::WebDriver::Actions: multiple pause calls in a row not (yet) supported"
1352 if $duration;
1353
1354 $self
1355 }
1356
1357 =item $al = $al->pointer_down ($button, $source)
1358
1359 =item $al = $al->pointer_up ($button, $source)
1360
1361 Press or release the given button. C<$button> defaults to C<0>.
1362
1363 =item $al = $al->click ($button, $source)
1364
1365 Convenience function that creates a button press and release action
1366 without any delay between them. C<$button> defaults to C<0>.
1367
1368 =item $al = $al->doubleclick ($button, $source)
1369
1370 Convenience function that creates two button press and release action
1371 pairs in a row, with no unnecessary delay between them. C<$button>
1372 defaults to C<0>.
1373
1374 =cut
1375
1376 sub pointer_down {
1377 my ($self, $button, $source) = @_;
1378
1379 $self->_add ($source, ptr => pointerDown => button => ($button // 0)*1)
1380 }
1381
1382 sub pointer_up {
1383 my ($self, $button, $source) = @_;
1384
1385 $self->_add ($source, ptr => pointerUp => button => ($button // 0)*1)
1386 }
1387
1388 sub click {
1389 my ($self, $button, $source) = @_;
1390
1391 $self
1392 ->pointer_down ($button, $source)
1393 ->pointer_up ($button)
1394 }
1395
1396 sub doubleclick {
1397 my ($self, $button, $source) = @_;
1398
1399 $self
1400 ->click ($button, $source)
1401 ->click ($button)
1402 }
1403
1404 =item $al = $al->move ($origin, $x, $y, $duration, $source)
1405
1406 Moves a pointer to the given position, relative to origin (either
1407 "viewport", "pointer" or an element object. The coordinates will be
1408 truncated to integer values.
1409
1410 =cut
1411
1412 sub move {
1413 my ($self, $origin, $x, $y, $duration, $source) = @_;
1414
1415 $self->_add ($source, ptr => pointerMove =>
1416 origin => $origin, x => int $x*1, y => int $y*1, duration => $duration*1)
1417 }
1418
1419 =item $al = $al->cancel ($source)
1420
1421 Executes a pointer cancel action.
1422
1423 =cut
1424
1425 sub cancel {
1426 my ($self, $source) = @_;
1427
1428 $self->_add ($source, ptr => "pointerCancel")
1429 }
1430
1431 =item $al = $al->key_down ($key, $source)
1432
1433 =item $al = $al->key_up ($key, $source)
1434
1435 Press or release the given key.
1436
1437 =item $al = $al->key ($key, $source)
1438
1439 Peess and release the given key in one go, without unnecessary delay.
1440
1441 A special syntax, C<{keyname}> can be used for special keys -
1442 all the special key names from L<the second table in section
1443 17.4.2|https://www.w3.org/TR/webdriver1/#keyboard-actions> of the
1444 WebDriver recommendation can be used - prefix with C<Shift-Space>. to get
1445 the shifted version, as in C<Shift-
1446
1447 Example: press and release "a".
1448
1449 $al->key ("a");
1450
1451 Example: press and release the "Enter" key:
1452
1453 $al->key ("\x{e007}");
1454
1455 Example: press and release the "enter" key using the special key name syntax:
1456
1457 $al->key ("{Enter}");
1458
1459 =item $al = $al->type ($string, $source)
1460
1461 Convenience method to simulate a series of key press and release events
1462 for the keys in C<$string>, one pair per extended unicode grapheme
1463 cluster. There is no syntax for special keys, everything will be typed
1464 "as-is" if possible.
1465
1466 =cut
1467
1468 # copy&paste form the spec via browser, with OSxxx replaces by Metaxxx
1469 our $SPECIAL_KEY = <<'EOF';
1470 "`" "~" "Backquote"
1471 "\" "|" "Backslash"
1472 "\uE003" "Backspace"
1473 "[" "{" "BracketLeft"
1474 "]" "}" "BracketRight"
1475 "," "<" "Comma"
1476 "0" ")" "Digit0"
1477 "1" "!" "Digit1"
1478 "2" "@" "Digit2"
1479 "3" "#" "Digit3"
1480 "4" "$" "Digit4"
1481 "5" "%" "Digit5"
1482 "6" "^" "Digit6"
1483 "7" "&" "Digit7"
1484 "8" "*" "Digit8"
1485 "9" "(" "Digit9"
1486 "=" "+" "Equal"
1487 "<" ">" "IntlBackslash"
1488 "a" "A" "KeyA"
1489 "b" "B" "KeyB"
1490 "c" "C" "KeyC"
1491 "d" "D" "KeyD"
1492 "e" "E" "KeyE"
1493 "f" "F" "KeyF"
1494 "g" "G" "KeyG"
1495 "h" "H" "KeyH"
1496 "i" "I" "KeyI"
1497 "j" "J" "KeyJ"
1498 "k" "K" "KeyK"
1499 "l" "L" "KeyL"
1500 "m" "M" "KeyM"
1501 "n" "N" "KeyN"
1502 "o" "O" "KeyO"
1503 "p" "P" "KeyP"
1504 "q" "Q" "KeyQ"
1505 "r" "R" "KeyR"
1506 "s" "S" "KeyS"
1507 "t" "T" "KeyT"
1508 "u" "U" "KeyU"
1509 "v" "V" "KeyV"
1510 "w" "W" "KeyW"
1511 "x" "X" "KeyX"
1512 "y" "Y" "KeyY"
1513 "z" "Z" "KeyZ"
1514 "-" "_" "Minus"
1515 "." ">"." "Period"
1516 "'" """ "Quote"
1517 ";" ":" "Semicolon"
1518 "/" "?" "Slash"
1519 "\uE00A" "AltLeft"
1520 "\uE052" "AltRight"
1521 "\uE009" "ControlLeft"
1522 "\uE051" "ControlRight"
1523 "\uE006" "Enter"
1524 "\uE03D" "OSLeft"
1525 "\uE053" "OSRight"
1526 "\uE008" "ShiftLeft"
1527 "\uE050" "ShiftRight"
1528 " " "\uE00D" "Space"
1529 "\uE004" "Tab"
1530 "\uE017" "Delete"
1531 "\uE010" "End"
1532 "\uE002" "Help"
1533 "\uE011" "Home"
1534 "\uE016" "Insert"
1535 "\uE00F" "PageDown"
1536 "\uE00E" "PageUp"
1537 "\uE015" "ArrowDown"
1538 "\uE012" "ArrowLeft"
1539 "\uE014" "ArrowRight"
1540 "\uE013" "ArrowUp"
1541 "\uE00C" "Escape"
1542 "\uE031" "F1"
1543 "\uE032" "F2"
1544 "\uE033" "F3"
1545 "\uE034" "F4"
1546 "\uE035" "F5"
1547 "\uE036" "F6"
1548 "\uE037" "F7"
1549 "\uE038" "F8"
1550 "\uE039" "F9"
1551 "\uE03A" "F10"
1552 "\uE03B" "F11"
1553 "\uE03C" "F12"
1554 "\uE01A" "\uE05C" "Numpad0"
1555 "\uE01B" "\uE056" "Numpad1"
1556 "\uE01C" "\uE05B" "Numpad2"
1557 "\uE01D" "\uE055" "Numpad3"
1558 "\uE01E" "\uE058" "Numpad4"
1559 "\uE01F" "Numpad5"
1560 "\uE020" "\uE05A" "Numpad6"
1561 "\uE021" "\uE057" "Numpad7"
1562 "\uE022" "\uE059" "Numpad8"
1563 "\uE023" "\uE054" "Numpad9"
1564 "\uE025" "NumpadAdd"
1565 "\uE026" "NumpadComma"
1566 "\uE028" "\uE05D" "NumpadDecimal"
1567 "\uE029" "NumpadDivide"
1568 "\uE007" "NumpadEnter"
1569 "\uE024" "NumpadMultiply"
1570 "\uE027" "NumpadSubtract"
1571
1572 "\uE03D" "MetaLeft"
1573 "\uE053" "MetaRight"
1574 EOF
1575
1576 our %SPECIAL_KEY;
1577
1578 sub _special_key($) {
1579 # parse first time
1580 %SPECIAL_KEY || do {
1581 for (split /\n/, $SPECIAL_KEY) {
1582 s/"//g or next;
1583 my ($k, $s, $name) = split /\t/;
1584
1585 # unescape \uXXXX, convert string to codepoint
1586 $_ = /^\\u/ ? hex substr $_, 2 : ord
1587 for $k, $s;
1588
1589 $SPECIAL_KEY{$name} = $k;
1590 $SPECIAL_KEY{"Shift-$name"} = $s if $s;
1591
1592 }
1593
1594 undef $SPECIAL_KEY; # save memory
1595 };
1596
1597 exists $SPECIAL_KEY{$_[0]}
1598 ? chr $SPECIAL_KEY{$_[0]}
1599 : Carp::croak "AnyEvent::WebDriver::Actions: special key '$1' not known"
1600 }
1601
1602 sub _kv($) {
1603 $_[0] =~ /^\{(.*)\}$/s
1604 ? _special_key $1
1605 : $_[0]
1606 }
1607
1608 sub key_down {
1609 my ($self, $key, $source) = @_;
1610
1611 $self->_add ($source, kbd => keyDown => value => _kv $key)
1612 }
1613
1614 sub key_up {
1615 my ($self, $key, $source) = @_;
1616
1617 $self->_add ($source, kbd => keyUp => value => _kv $key)
1618 }
1619
1620 sub key {
1621 my ($self, $key, $source) = @_;
1622
1623 $self
1624 ->key_down ($key, $source)
1625 ->key_up ($key)
1626 }
1627
1628 sub type {
1629 my ($self, $string, $source) = @_;
1630
1631 $self->key ($_, $source)
1632 for $string =~ /(\X)/g;
1633
1634 $self
1635 }
1636
1637 =item $al->perform ($wd)
1638
1639 Finalises and compiles the list, if not done yet, and calls C<<
1640 $wd->perform >> with it.
1641
1642 If C<$wd> is undef, and the action list was created using the C<<
1643 $wd->actions >> method, then perform it against that WebDriver object.
1644
1645 There is no underscore variant - call the C<perform_actions_> method with
1646 the action object instead.
1647
1648 =item $al->perform_release ($wd)
1649
1650 Exactly like C<perform>, but additionally call C<release_actions>
1651 afterwards.
1652
1653 =cut
1654
1655 sub perform {
1656 my ($self, $wd) = @_;
1657
1658 ($wd //= $self->{wd})->perform_actions ($self)
1659 }
1660
1661 sub perform_release {
1662 my ($self, $wd) = @_;
1663
1664 ($wd //= $self->{wd})->perform_actions ($self);
1665 $wd->release_actions;
1666 }
1667
1668 =item ($actions, $duration) = $al->compile
1669
1670 Finalises and compiles the list, if not done yet, and returns an actions
1671 object suitable for calls to C<< $wd->perform_actions >>. When called in
1672 list context, additionally returns the total duration of the action list.
1673
1674 Since building large action lists can take nontrivial amounts of time,
1675 it can make sense to build an action list only once and then perform it
1676 multiple times.
1677
1678 No additional actions must be added after compiling an action list.
1679
1680 =cut
1681
1682 sub compile {
1683 my ($self) = @_;
1684
1685 $self->{duration} += delete $self->{tick_duration};
1686
1687 delete $self->{tick};
1688 delete $self->{last_kbd};
1689 delete $self->{last_ptr};
1690
1691 $self->{actions} ||= [values %{ delete $self->{source} }];
1692
1693 wantarray
1694 ? ($self->{actions}, $self->{duration})
1695 : $self->{actions}
1696 }
1697
1698 =back
1699
1700 =head2 EVENT BASED API
1701
1702 This module wouldn't be a good AnyEvent citizen if it didn't have a true
1703 event-based API.
1704
1705 In fact, the simplified API, as documented above, is emulated via the
1706 event-based API and an C<AUTOLOAD> function that automatically provides
1707 blocking wrappers around the callback-based API.
1708
1709 Every method documented in the L<SIMPLIFIED API> section has an equivalent
1710 event-based method that is formed by appending a underscore (C<_>) to the
1711 method name, and appending a callback to the argument list (mnemonic: the
1712 underscore indicates the "the action is not yet finished" after the call
1713 returns).
1714
1715 For example, instead of a blocking calls to C<new_session>, C<navigate_to>
1716 and C<back>, you can make a callback-based ones:
1717
1718 my $cv = AE::cv;
1719
1720 $wd->new_session ({}, sub {
1721 my ($status, $value) = @_,
1722
1723 die "error $value->{error}" if $status ne "200";
1724
1725 $wd->navigate_to_ ("http://www.nethype.de", sub {
1726
1727 $wd->back_ (sub {
1728 print "all done\n";
1729 $cv->send;
1730 });
1731
1732 });
1733 });
1734
1735 $cv->recv;
1736
1737 While the blocking methods C<croak> on errors, the callback-based ones all
1738 pass two values to the callback, C<$status> and C<$res>, where C<$status>
1739 is the HTTP status code (200 for successful requests, typically 4xx or
1740 5xx for errors), and C<$res> is the value of the C<value> key in the JSON
1741 response object.
1742
1743 Other than that, the underscore variants and the blocking variants are
1744 identical.
1745
1746 =head2 LOW LEVEL API
1747
1748 All the simplified API methods are very thin wrappers around WebDriver
1749 commands of the same name. They are all implemented in terms of the
1750 low-level methods (C<req>, C<get>, C<post> and C<delete>), which exist
1751 in blocking and callback-based variants (C<req_>, C<get_>, C<post_> and
1752 C<delete_>).
1753
1754 Examples are after the function descriptions.
1755
1756 =over
1757
1758 =item $wd->req_ ($method, $uri, $body, $cb->($status, $value))
1759
1760 =item $value = $wd->req ($method, $uri, $body)
1761
1762 Appends the C<$uri> to the C<endpoint/session/{sessionid}/> URL and makes
1763 a HTTP C<$method> request (C<GET>, C<POST> etc.). C<POST> requests can
1764 provide a UTF-8-encoded JSON text as HTTP request body, or the empty
1765 string to indicate no body is used.
1766
1767 For the callback version, the callback gets passed the HTTP status code
1768 (200 for every successful request), and the value of the C<value> key in
1769 the JSON response object as second argument.
1770
1771 =item $wd->get_ ($uri, $cb->($status, $value))
1772
1773 =item $value = $wd->get ($uri)
1774
1775 Simply a call to C<req_> with C<$method> set to C<GET> and an empty body.
1776
1777 =item $wd->post_ ($uri, $data, $cb->($status, $value))
1778
1779 =item $value = $wd->post ($uri, $data)
1780
1781 Simply a call to C<req_> with C<$method> set to C<POST> - if C<$body> is
1782 C<undef>, then an empty object is send, otherwise, C<$data> must be a
1783 valid request object, which gets encoded into JSON for you.
1784
1785 =item $wd->delete_ ($uri, $cb->($status, $value))
1786
1787 =item $value = $wd->delete ($uri)
1788
1789 Simply a call to C<req_> with C<$method> set to C<DELETE> and an empty body.
1790
1791 =cut
1792
1793 =back
1794
1795 Example: implement C<get_all_cookies>, which is a simple C<GET> request
1796 without any parameters:
1797
1798 $cookies = $wd->get ("cookie");
1799
1800 Example: implement C<execute_script>, which needs some parameters:
1801
1802 $results = $wd->post ("execute/sync" => { script => "$javascript", args => [] });
1803
1804 Example: call C<find_elements> to find all C<IMG> elements:
1805
1806 $elems = $wd->post (elements => { using => "css selector", value => "img" });
1807
1808 =cut
1809
1810 =head1 HISTORY
1811
1812 This module was unintentionally created (it started inside some quickly
1813 hacked-together script) simply because I couldn't get the existing
1814 C<Selenium::Remote::Driver> module to work reliably, ever, despite
1815 multiple attempts over the years and trying to report multiple bugs, which
1816 have been completely ignored. It's also not event-based, so, yeah...
1817
1818 =head1 AUTHOR
1819
1820 Marc Lehmann <schmorp@schmorp.de>
1821 http://anyevent.schmorp.de
1822
1823 =cut
1824
1825 1
1826