ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-WebDriver/WebDriver.pm
Revision: 1.16
Committed: Wed Aug 29 05:06:49 2018 UTC (5 years, 9 months ago) by root
Branch: MAIN
Changes since 1.15: +5 -0 lines
Log Message:
*** empty log message ***

File Contents

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