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