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