AnyEvent-WebDriver

 view release on metacpan or  search on metacpan

WebDriver.pm  view on Meta::CPAN

      body => $body,
      $self->{persistent} ? (persistent => 1) : (),
      $self->{proxy} eq "default" ? () : (proxy => $self->{proxy}),
      timeout => $self->{timeout},
      headers => { "content-type" => "application/json; charset=utf-8", "cache-control" => "no-cache" },
      sub {
         my ($res, $hdr) = @_;

         $res = eval { $json->decode ($res) };
         $hdr->{Status} = 500 unless exists $res->{value};

         $cb->($hdr->{Status}, $res->{value});
      }
   ;
}

sub get_ {
   my ($self, $ep, $cb) = @_;

   $self->req_ (GET => $ep, undef, $cb)
}

sub post_ {
   my ($self, $ep, $data, $cb) = @_;

   $self->req_ (POST => $ep, $json->encode ($data || {}), $cb)
}

sub delete_ {
   my ($self, $ep, $cb) = @_;

   $self->req_ (DELETE => $ep, "", $cb)
}

sub AUTOLOAD {
   our $AUTOLOAD;

   $_[0]->isa (__PACKAGE__)
      or Carp::croak "$AUTOLOAD: no such function";

   (my $name = $AUTOLOAD) =~ s/^.*://;

   my $name_ = "$name\_";

   defined &$name_
      or Carp::croak "$AUTOLOAD: no such method";

   my $func_ = \&$name_;

   *$name = sub {
      $func_->(@_, my $cv = AE::cv);
      my ($status, $res) = $cv->recv;

      if ($status ne "200") {
         my $msg;

         if (exists $res->{error}) {
            $msg = "AyEvent::WebDriver: $res->{error}: $res->{message}";
            $msg .= "\n$res->{stacktrace}caught at" if length $res->{stacktrace};
         } else {
            $msg = "AnyEvent::WebDriver: http status $status (wrong endpoint?), caught";
         }

         Carp::croak $msg;
      }

      $res
   };

   goto &$name;
}

=head2 WEBDRIVER OBJECTS

=over

=item new AnyEvent::WebDriver key => value...

Create a new WebDriver object. Example for a remote WebDriver connection
(the only type supported at the moment):

   my $wd = new AnyEvent::WebDriver endpoint => "http://localhost:4444";

Supported keys are:

=over

=item endpoint => $string

For remote connections, the endpoint to connect to (defaults to C<http://localhost:4444>).

=item proxy => $proxyspec

The proxy to use (same as the C<proxy> argument used by
L<AnyEvent::HTTP>). The default is C<undef>, which disables proxies. To
use the system-provided proxy (e.g. C<http_proxy> environment variable),
specify the string C<default>.

=item autodelete => $boolean

If true (the default), then automatically execute C<delete_session> when
the WebDriver object is destroyed with an active session. If set to a
false value, then the session will continue to exist.

Note that due to bugs in perl that are unlikely to get fixed,
C<autodelete> is likely ineffective during global destruction and might
even crash your process, so you should ensure objects go out of scope
before that, or explicitly call C<delete_session>, if you want the session
to be cleaned up.

=item timeout => $seconds

The HTTP timeout, in (fractional) seconds (default: C<300>). This timeout
is reset on any activity, so it is not an overall request timeout. Also,
individual requests might extend this timeout if they are known to take
longer.

=item persistent => C<1> | C<undef>

If true (the default) then persistent connections will be used for all
requests, which assumes you have a reasonably stable connection (such as
to C<localhost> :) and that the WebDriver has a persistent timeout much
higher than what L<AnyEvent::HTTP> uses.

You can force connections to be closed for non-idempotent requests (the
safe default of L<AnyEvent::HTTP>) by setting this to C<undef>.

=back

=cut

sub new {
   my ($class, %kv) = @_;

   bless {
      endpoint   => "http://localhost:4444",
      proxy      => undef,
      persistent => 1,
      autodelete => 1,
      timeout    => 300,
      %kv,
   }, $class
}

sub DESTROY {
   my ($self) = @_;

   $self->delete_session
      if exists $self->{sid} && $self->{autodelete};
}

=item $al = $wd->actions

Creates an action list associated with this WebDriver. See L<ACTION
LISTS>, below, for full details.

=cut

sub actions {
   AnyEvent::WebDriver::Actions->new (wd => $_[0])
}

=item $sessionstring = $wd->save_session

Save the current session in a string so it can be restored load with
C<load_session>. Note that only the session data itself is stored
(currently the session id and capabilities), not the endpoint information
itself.

The main use of this function is in conjunction with disabled
C<autodelete>, to save a session to e.g., and restore it later. It could
presumably used for other applications, such as using the same session
from multiple processes and so on.

=item $wd->load_session ($sessionstring)

=item $wd->set_session ($sessionid, $capabilities)

Starts using the given session, as identified by
C<$sessionid>. C<$capabilities> should be the original session
capabilities, although the current version of this module does not make
any use of it.

The C<$sessionid> is stored in C<< $wd->{sid} >> (and could be fetched
form there for later use), while the capabilities are stored in C<<
$wd->{capabilities} >>.

=cut

sub save_session {
   my ($self) = @_;

   $json->encode ([1, $self->{sid}, $self->{capabilities}]);
}

sub load_session {
   my ($self, $session) = @_;

   $session = $json->decode ($session);

   $session->[0] == 1
      or Carp::croak "AnyEvent::WebDriver::load_session: session corrupted or from different version";

   $self->set_session ($session->[1], $session->[2]);
}

sub set_session {
   my ($self, $sid, $caps) = @_;

   $self->{sid}          = $sid;
   $self->{capabilities} = $caps;

   $self->{_ep} = "$self->{endpoint}/session/$self->{sid}/";
}

=back

=head2 SIMPLIFIED API

This section documents the simplified API, which is really just a very
thin wrapper around the WebDriver protocol commands. They all block the
caller until the result is available (using L<AnyEvent> condvars), so must
not be called from an event loop callback - see L<EVENT BASED API> for an
alternative.

The method names are pretty much taken directly from the W3C WebDriver
specification, e.g. the request documented in the "Get All Cookies"
section is implemented via the C<get_all_cookies> method.

The order is the same as in the WebDriver draft at the time of this
writing, and only minimal massaging is done to request parameters and
results.

=head3 SESSIONS

=over

=cut

=item $wd->new_session ({ key => value... })

Try to connect to the WebDriver and initialize a new session with a
"new session" command, passing the given key-value pairs as value
(e.g. C<capabilities>).

No session-dependent methods must be called before this function returns
successfully, and only one session can be created per WebDriver object.

On success, C<< $wd->{sid} >> is set to the session ID, and C<<
$wd->{capabilities} >> is set to the returned capabilities.

Simple example of creating a WebDriver object and a new session:

   my $wd = new AnyEvent::WebDriver endpoint => "http://localhost:4444";
   $wd->new_session ({});

Real-world example with capability negotiation:

   $wd->new_session ({
      capabilities => {
         alwaysMatch => {
            pageLoadStrategy        => "eager",
            unhandledPromptBehavior => "dismiss",
            # proxy => { proxyType => "manual", httpProxy => "1.2.3.4:56", sslProxy => "1.2.3.4:56" },
         },
         firstMatch => [
            {
               browserName => "firefox",
               "moz:firefoxOptions" => {
                  binary => "firefox/firefox",
                  args => ["-devtools", "-headless"],
                  prefs => {
                     "dom.webnotifications.enabled" => \0,
                     "dom.push.enabled" => \0,
                     "dom.disable_beforeunload" => \1,
                     "browser.link.open_newwindow" => 3,
                     "browser.link.open_newwindow.restrictions" => 0,
                     "dom.popup_allowed_events" => "",
                     "dom.disable_open_during_load" => \1,
                  },
               },
            },
            {
               browserName => "chrome",
               "goog:chromeOptions" => {
                  binary => "/bin/chromium",
                  args => ["--no-sandbox", "--headless"],
                  prefs => {
                     # ...
                  },
               },
            },
            {
               # generic fallback
            },
         ],

      },
   });

Firefox-specific capability documentation can be found L<on
MDN|https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities>,
Chrome-specific capability documentation might be found
L<here|http://chromedriver.chromium.org/capabilities>, but the latest
release at the time of this writing (chromedriver 77) has essentially
no documentation about webdriver capabilities (even MDN has better
documentation about chromwedriver!)

If you have URLs for Safari/IE/Edge etc. capabilities, feel free to tell
me about them.

=cut

sub new_session_ {
   my ($self, $kv, $cb) = @_;

   $kv->{capabilities} ||= {}; # required by protocol

   local $self->{_ep} = "$self->{endpoint}/";
   $self->post_ (session => $kv, sub {
      my ($status, $res) = @_;

      exists $res->{capabilities}
         or $status = "500"; # blasted chromedriver

      $self->set_session ($res->{sessionId}, $res->{capabilities})
         if $status eq "200";

      $cb->($status, $res);
   });
}

=item $wd->delete_session

Deletes the session - the WebDriver object must not be used after this
call (except for calling this method).

This method is always safe to call and will not do anything if there is no
active session.

=cut

sub delete_session_ {
   my ($self, $cb) = @_;

   my $sid = delete $self->{sid};
   delete $self->{capoabilities};

   return unless defined $sid;

   local $self->{_ep} = "$self->{endpoint}/session/$sid";
   $self->delete_ ("" => $cb);
}

=item $timeouts = $wd->get_timeouts

Get the current timeouts, e.g.:

   my $timeouts = $wd->get_timeouts;
   => { implicit => 0, pageLoad => 300000, script => 30000 }

=item $wd->set_timeouts ($timeouts)

Sets one or more timeouts, e.g.:

   $wd->set_timeouts ({ script => 60000 });

=cut

sub get_timeouts_ {
   $_[0]->get_ (timeouts => $_[1], $_[2]);
}

sub set_timeouts_ {
   $_[0]->post_ (timeouts => $_[1], $_[2], $_[3]);
}

=back

=head3 NAVIGATION

=over

=cut

=item $wd->navigate_to ($url)

Navigates to the specified URL.

=item $url = $wd->get_current_url

Queries the current page URL as set by C<navigate_to>.

=cut

sub navigate_to_ {
   $_[0]->post_ (url => { url => "$_[1]" }, $_[2]);
}

sub get_current_url_ {
   $_[0]->get_ (url => $_[1])
}

=item $wd->back

The equivalent of pressing "back" in the browser.

=item $wd->forward

The equivalent of pressing "forward" in the browser.

WebDriver.pm  view on Meta::CPAN

This module wouldn't be a good AnyEvent citizen if it didn't have a true
event-based API.

In fact, the simplified API, as documented above, is emulated via the
event-based API and an C<AUTOLOAD> function that automatically provides
blocking wrappers around the callback-based API.

Every method documented in the L<SIMPLIFIED API> section has an equivalent
event-based method that is formed by appending a underscore (C<_>) to the
method name, and appending a callback to the argument list (mnemonic: the
underscore indicates the "the action is not yet finished" after the call
returns).

For example, instead of a blocking calls to C<new_session>, C<navigate_to>
and C<back>, you can make a callback-based ones:

   my $cv = AE::cv;

   $wd->new_session ({}, sub {
      my ($status, $value) = @_,

      die "error $value->{error}" if $status ne "200";

      $wd->navigate_to_ ("http://www.nethype.de", sub {

         $wd->back_ (sub {
            print "all done\n";
            $cv->send;
         });

      });
   });

   $cv->recv;

While the blocking methods C<croak> on errors, the callback-based ones all
pass two values to the callback, C<$status> and C<$res>, where C<$status>
is the HTTP status code (200 for successful requests, typically 4xx or
5xx for errors), and C<$res> is the value of the C<value> key in the JSON
response object.

Other than that, the underscore variants and the blocking variants are
identical.

=head2 LOW LEVEL API

All the simplified API methods are very thin wrappers around WebDriver
commands of the same name. They are all implemented in terms of the
low-level methods (C<req>, C<get>, C<post> and C<delete>), which exist
in blocking and callback-based variants (C<req_>, C<get_>, C<post_> and
C<delete_>).

Examples are after the function descriptions.

=over

=item $wd->req_ ($method, $uri, $body, $cb->($status, $value))

=item $value = $wd->req ($method, $uri, $body)

Appends the C<$uri> to the C<endpoint/session/{sessionid}/> URL and makes
a HTTP C<$method> request (C<GET>, C<POST> etc.). C<POST> requests can
provide a UTF-8-encoded JSON text as HTTP request body, or the empty
string to indicate no body is used.

For the callback version, the callback gets passed the HTTP status code
(200 for every successful request), and the value of the C<value> key in
the JSON response object as second argument.

=item $wd->get_ ($uri, $cb->($status, $value))

=item $value = $wd->get ($uri)

Simply a call to C<req_> with C<$method> set to C<GET> and an empty body.

=item $wd->post_ ($uri, $data, $cb->($status, $value))

=item $value = $wd->post ($uri, $data)

Simply a call to C<req_> with C<$method> set to C<POST> - if C<$body> is
C<undef>, then an empty object is send, otherwise, C<$data> must be a
valid request object, which gets encoded into JSON for you.

=item $wd->delete_ ($uri, $cb->($status, $value))

=item $value = $wd->delete ($uri)

Simply a call to C<req_> with C<$method> set to C<DELETE> and an empty body.

=cut

=back

Example: implement C<get_all_cookies>, which is a simple C<GET> request
without any parameters:

   $cookies = $wd->get ("cookie");

Example: implement C<execute_script>, which needs some parameters:

   $results = $wd->post ("execute/sync" => { script => "$javascript", args => [] });

Example: call C<find_elements> to find all C<IMG> elements:

   $elems = $wd->post (elements => { using => "css selector", value => "img" });

=cut

=head1 HISTORY

This module was unintentionally created (it started inside some quickly
hacked-together script) simply because I couldn't get the existing
C<Selenium::Remote::Driver> module to work reliably, ever, despite
multiple attempts over the years and trying to report multiple bugs, which
have been completely ignored. It's also not event-based, so, yeah...

=head1 AUTHOR

   Marc Lehmann <schmorp@schmorp.de>
   http://anyevent.schmorp.de



( run in 1.039 second using v1.01-cache-2.11-cpan-df04353d9ac )