123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- package Selenium::Remote::Driver;
- use strict;
- use warnings;
- use Selenium::Remote::RemoteConnection;
- # ABSTRACT: Perl Client for Selenium Remote Driver
- =for Pod::Coverage BUILD
- =for Pod::Coverage DEMOLISH
- =head1 SYNOPSIS
- use Selenium::Remote::Driver;
- my $driver = Selenium::Remote::Driver->new;
- $driver->get('http://www.google.com');
- print $driver->get_title();
- $driver->quit();
- =cut
- =head1 DESCRIPTION
- Selenium is a test tool that allows you to write
- automated web application UI tests in any programming language against
- any HTTP website using any mainstream JavaScript-enabled browser. This module is
- an implementation of the client for the Remote driver that Selenium provides.
- You can find bindings for other languages at this location:
- L<https://www.seleniumhq.org/download/>
- This module sends commands directly to the Server using HTTP. Using this module
- together with the Selenium Server, you can automatically control any supported
- browser. To use this module, you need to have already downloaded and started
- the Selenium Server (Selenium Server is a Java application).
- =cut
- =head1 USAGE
- =head2 Without Standalone Server
- As of v0.25, it's possible to use this module without a standalone
- server - that is, you would not need the JRE or the JDK to run your
- Selenium tests. See L<Selenium::Chrome>, L<Selenium::PhantomJS>,
- L<Selenium::Edge>, L<Selenium::InternetExplorer>,and L<Selenium::Firefox>
- for details. If you'd like additional browsers besides these,
- give us a holler over in
- L<Github|https://github.com/teodesian/Selenium-Remote-Driver/issues>.
- =head2 Remote Driver Response
- Selenium::Remote::Driver uses the
- L<JsonWireProtocol|https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol>
- And the
- L<WC3 WebDriver Protocol|https://www.w3.org/TR/webdriver/>
- to communicate with the Selenium Server. If an error occurs while
- executing the command then the server sends back an HTTP error code
- with a JSON encoded reponse that indicates the precise
- L<Response Error Code|https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#response-status-codes>.
- The module will then croak with the error message associated with this
- code. If no error occurred, then the subroutine called will return the
- value sent back from the server (if a return value was sent).
- So a rule of thumb while invoking methods on the driver is if the method did not
- croak when called, then you can safely assume the command was successful even if
- nothing was returned by the method.
- =head2 WebElement
- Selenium Webdriver represents all the HTML elements as WebElement, which is
- in turn represented by L<Selenium::Remote::WebElement> module. So any method that
- deals with WebElements will return and/or expect WebElement object. The POD for
- that module describes all the methods that perform various actions on the
- WebElements like click, submit etc.
- To interact with any WebElement you have to first "find" it, read the POD for
- find_element or find_elements for further info. Once you find the required element
- then you can perform various actions. If you don't call find_* method first, all
- your further actions will fail for that element. Finally, just remember that you
- don't have to instantiate WebElement objects at all - they will be automatically
- created when you use the find_* methods.
- A sub-class of Selenium::Remote::WebElement may be used instead of Selenium::Remote::WebElement,
- by providing that class name as an option the constructor:
- my $driver = Selenium::Remote::Driver->new( webelement_class => ... );
- For example, a testing-subclass may extend the web-element object with testing methods.
- =head2 LWP Read Timeout errors
- It's possible to make Selenium calls that take longer than the default
- L<LWP::UserAgent> timeout. For example, setting the asynchronous
- script timeout greater than the LWP::UserAgent timeout and then
- executing a long running asynchronous snippet of javascript will
- immediately trigger an error like:
- Error while executing command: executeAsyncScript: Server returned
- error message read timeout at...
- You can get around this by configuring LWP's timeout value, either by
- constructing your own LWP and passing it in to ::Driver during
- instantiation
- my $timeout_ua = LWP::UserAgent->new;
- $timeout_ua->timeout(360); # this value is in seconds!
- my $d = Selenium::Remote::Driver->new( ua => $timeout_ua );
- or by configuring the timeout on the fly as necessary:
- use feature qw/say/;
- use Selenium::Remote::Driver;
- my $d = Selenium::Remote::Driver->new;
- say $d->ua->timeout; # 180 seconds is the default
- $d->ua->timeout(2); # LWP wants seconds, not milliseconds!
- $d->set_timeout('script', 1000); # S::R::D wants milliseconds!
- # Async scripts only return when the callback is invoked. Since there
- # is no callback here, Selenium will block for the entire duration of
- # the async timeout script. This will hit Selenium's async script
- # timeout before hitting LWP::UserAgent's read timeout
- $d->execute_async_script('return "hello"');
- $d->quit;
- =head1 TESTING
- If are writing automated tests using this module, you may be
- interested in L<Test::Selenium::Remote::Driver> which is also included
- in this distribution. It includes convenience testing methods for many
- of the selenum methods available here.
- Your other option is to use this module in conjunction with your
- choice of testing modules, like L<Test::Spec> or L<Test::More> as
- you please.
- =head1 WC3 WEBDRIVER COMPATIBILITY
- WC3 Webdriver is a constantly evolving standard, so some things may or may not work at any given time.
- Furthermore, out of date drivers probably identify as WD3, while only implementing a few methods and retaining JSONWire functionality.
- One way of dealing with this is setting:
- $driver->{is_wd3} = 0
- Of course, this will prevent access of any new WC3 methods, but will probably make your tests pass until your browser's driver gets it's act together.
- There are also some JSONWire behaviors that we emulate in methods, such as Selenium::Remote::WebElement::get_attribute.
- You can get around that by passing an extra flag to the sub, or setting:
- $driver->{emulate_jsonwire} = 0;
- When in WC3 Webdriver mode.
- =head2 FINDERS
- This constant is a hashref map of the old element finder aliases from wd2 to wd3.
- use Data::Dumper;
- print Dumper($Selenium::Remote::Driver::FINDERS);
- =head2 WC3 WEBDRIVER CURRENT STATUS
- That said, the following 'sanity tests' in the at/ (acceptance test) directory of the module passed on the following versions:
- =over 4
- =item Selenium Server: 3.8.1 - all tests
- =item geckodriver: 0.19.1 - at/sanity.test, at/firefox.test (Selenium::Firefox)
- =item chromedriver: 2.35 - at/sanity-chrome.test, at/chrome.test (Selenium::Chrome)
- =item edgedriver: 5.16299 - at/sanity-edge.test
- =item InternetExplorerDriver : 3.8.1 - at/sanity-ie.test (be sure to enable 'allow local files to run active content in your 'advanced settings' pane)
- =item safaridriver : 11.0.2 - at/sanity-safari.test (be sure to enable 'allow automated testing' in the developer menu) -- it appears WC3 spec is *unimplemented*
- =back
- These tests are intended to be run directly against a working selenium server on the local host with said drivers configured.
- If you are curious as to what 'works and does not' on your driver versions (and a few other quirks),
- it is strongly encouraged you look at where the test calls the methods you are interested in.
- While other browsers/drivers (especially legacy ones) likely work fine as well,
- any new browser/driver will likely have problems if it's not listed above.
- There is also a 'legacy.test' file available to run against old browsers/selenium (2.x servers, pre geckodriver).
- This should only be used to verify backwards-compatibility has not been broken.
- =head2 Firefox Notes
- If you are intending to pass extra_capabilities to firefox on a WD3 enabled server with geckodriver, you MUST do the following:
- $Selenium::Remote::Driver::FORCE_WD3=1;
- This is because the gecko driver prefers legacy capabilities, both of which are normally passed for compatibility reasons.
- =head2 Chrome Notes
- Use the option goog:chromeOptions instead of chromeOptions, if you are intending to pass extra_capabilities on a
- WD3 enabled server with chromedriver enabled.
- https://sites.google.com/a/chromium.org/chromedriver/capabilities
- Also, if you instantiate the object in WC3 mode (which is the default), the remote driver will throw exceptions you have no choice but to catch,
- rather than falling back to JSONWire methods where applicable like geckodriver does.
- As of chrome 75 (and it's appropriate driver versions), the WC3 spec has finally been implemented.
- As such, to use chrome older than this, you will have to manually force on JSONWire mode:
- $Selenium::Remote::Driver::FORCE_WD2=1;
- =head2 Notes on Running Selenium at Scale via selenium.jar
- When running many, many tests in parallel you can eventually reach resource exhaustion.
- You have to instruct the Selenium JAR to do some cleanup to avoid explosions:
- Inside of your selenium server's node.json (running a grid), you would put in the following:
- "configuration" :
- {
- "cleanUpCycle":2000
- }
- Or run the selenium jar with the -cleanupCycle parameter. Of course use whatever # of seconds is appropriate to your situation.
- =head1 CONSTRUCTOR
- =head2 new
- Dies if communication with the selenium server cannot be established.
- Input: (all optional)
- Desired capabilities - HASH - Following options are accepted:
- =over 4
- =item B<remote_server_addr> - <string> - IP or FQDN of the Webdriver server machine. Default: 'localhost'
- =item B<port> - <string> - Port on which the Webdriver server is listening. Default: 4444
- =item B<browser_name> - <string> - desired browser string: {phantomjs|firefox|internet explorer|MicrosoftEdge|safari|htmlunit|iphone|chrome}
- =item B<version> - <string> - desired browser version number
- =item B<platform> - <string> - desired platform: {WINDOWS|XP|VISTA|MAC|LINUX|UNIX|ANY}
- =item B<accept_ssl_certs> - <boolean> - whether SSL certs should be accepted, default is true.
- =item B<firefox_profile> - Profile - Use Selenium::Firefox::Profile to create a Firefox profile for the browser to use.
- =item B<javascript> - <boolean> - Whether or not to use Javascript. You probably won't disable this, as you would be using L<WWW::Mechanize> instead. Default: True
- =item B<auto_close> - <boolean> - Whether to automatically close the browser session on the server when the object goes out of scope. Default: False.
- =item B<default_finder> - <string> - Default method by which to evaluate selectors. Default: 'xpath'
- =item B<session_id> - <string> - Provide a Session ID to highjack a browser session on the remote server. Useful for micro-optimizers. Default: undef
- =item B<pageLoadStrategy> - STRING - OPTIONAL, 'normal|eager|none'. default 'normal'. WebDriver3 only.
- =item B<extra_capabilities> - HASH - Any other extra capabilities. Accepted keys will vary by browser. If firefox_profile is passed, the args (or profile) key will be overwritten, depending on how it was passed.
- =item B<debug> - BOOL - Turn Debug mode on from the start if true, rather than having to call debug_on().
- =back
- On WebDriver3 the 'extra_capabilities' will be automatically converted into the parameter needed by your browser.
- For example, extra_capabilities is passed to the server as the moz:firefoxOptions parameter.
- You can also specify some options in the constructor hash that are
- not part of the browser-related desired capabilities.
- =over 4
- =item B<auto_close> - <boolean> - whether driver should end session on remote server on close.
- =item B<base_url> - <string> - OPTIONAL, base url for the website Selenium acts on. This can save you from repeating the domain in every call to $driver->get()
- =item B<default_finder> - <string> - choose default finder used for find_element* {class|class_name|css|id|link|link_text|name|partial_link_text|tag_name|xpath}
- =item B<inner_window_size> - <aref[Int]> - An array ref [ height, width ] that the browser window should use as its initial size immediately after instantiation
- =item B<error_handler> - CODEREF - A CODEREF that we will call in event of any exceptions. See L</error_handler> for more details.
- =item B<webelement_class> - <string> - sub-class of Selenium::Remote::WebElement if you wish to use an alternate WebElement class.
- =item B<ua> - LWP::UserAgent instance - if you wish to use a specific $ua, like from Test::LWP::UserAgent
- =item B<proxy> - HASH - Proxy configuration with the following keys:
- =over 4
- =item B<proxyType> - <string> - REQUIRED, Possible values are:
- direct - A direct connection - no proxy in use,
- manual - Manual proxy settings configured, e.g. setting a proxy for HTTP, a proxy for FTP, etc,
- pac - Proxy autoconfiguration from a URL,
- autodetect - proxy autodetection, probably with WPAD,
- system - Use system settings
- =item B<proxyAutoconfigUrl> - <string> - REQUIRED if proxyType is 'pac', ignored otherwise. Expected format: http://hostname.com:1234/pacfile or file:///path/to/pacfile
- =item B<ftpProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234
- =item B<httpProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234
- =item B<sslProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234
- =item B<socksProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234. WebDriver 3 only.
- =item B<socksVersion> - <int> - OPTIONAL, ignored if proxyType is not 'manual'. WebDriver 3 only.
- =item B<noProxy> - <ARRAY> - OPTIONAL, list of URLs to bypass the proxy for. WebDriver3 only.
- =item B<firefox_profile> - <string> - Base64 encoded ZIP file of a firefox profile directory, for use when you don't want/need Selenium::Firefox::Profile.
- =back
- =back
- Output:
- Selenium::Remote::Driver object
- Usage:
- my $driver = Selenium::Remote::Driver->new;
- #or
- my $driver = Selenium::Remote::Driver->new('browser_name' => 'firefox',
- 'platform' => 'MAC');
- #or (for Firefox 47 or lower on Selenium 3+)
- my $driver = Selenium::Remote::Driver->new('browser_name' => 'firefox',
- 'platform' => 'MAC',
- 'extra_capabilities' => {
- 'marionette' => \0,
- });
- #or
- my $driver = Selenium::Remote::Driver->new('remote_server_addr' => '10.10.1.1',
- 'port' => '2222',
- 'auto_close' => 0);
- #or
- my $driver = Selenium::Remote::Driver->new('browser_name' =>'chrome',
- 'extra_capabilities' => {
- 'goog:chromeOptions' => {
- 'args' => [
- 'window-size=1260,960',
- 'incognito'
- ],
- 'prefs' => {
- 'session' => {
- 'restore_on_startup' => 4,
- 'urls_to_restore_on_startup' => [
- 'http://www.google.com',
- 'http://docs.seleniumhq.org'
- ]},
- 'first_run_tabs' => [
- 'http://www.google.com',
- 'http://docs.seleniumhq.org'
- ]
- }
- }
- });
- #or
- my $driver = Selenium::Remote::Driver->new('proxy' => {'proxyType' => 'manual', 'httpProxy' => 'myproxy.com:1234'});
- #or
- my $driver = Selenium::Remote::Driver->new('default_finder' => 'css');
- =head3 error_handler
- =head3 clear_error_handler
- OPTIONAL constructor arg & associated setter/clearer: if you wish to
- install your own error handler, you may pass a code ref in to
- C<error_handler> during instantiation like follows:
- my $driver = Selenium::Remote::Driver->new(
- error_handler => sub { print $_[1]; croak 'goodbye'; }
- );
- Additionally, you can set and/or clear it at any time on an
- already-instantiated driver:
- # later, change the error handler to something else
- $driver->error_handler( sub { print $_[1]; croak 'hello'; } );
- # stop handling errors manually and use the default S:R:D behavior
- # (we will croak about the exception)
- $driver->clear_error_handler;
- Your error handler will receive three arguments: the first argument is
- the C<$driver> object itself, and the second argument is the exception
- message and stack trace in one multiline string. The final argument(s) are the
- argument array to the command just executed.
- B<N.B.>: If you set your own error handler, you are entirely
- responsible for handling webdriver exceptions, _including_ croaking
- behavior. That is, when you set an error handler, we will no longer
- croak on Webdriver exceptions - it's up to you to do so. For
- consistency with the standard S:R:D behavior, we recommend your error
- handler also croak when it's done, especially since your test
- shouldn't be running into unexpected errors. Catching specific or
- desired errors in your error handler makes sense, but not croaking at
- all can leave you in unfamiliar territory. Reaching an unexpected
- exception might mean your test has gone off the rails, and the further
- your test gets from the source of the of the exception, the harder it
- will be to debug.
- B<N.B.>: Four methods will still croak on their own: L</find_element>,
- L</find_elements>, L</find_child_element>, and
- L</find_child_elements>. If these methods throw a Webdriver Exception,
- your error handler _will still be_ invoked inside an C<eval>, and then
- they'll croak with their own error message that indicates the locator
- and strategy used. So, your strategies for avoiding exceptions when
- finding elements do not change (either use find_elements and check
- the returned array size, wrap your calls to find_element* in an
- C<eval>, or use the parameterized versions find_element_*).
- =head2 new_from_caps
- Description:
- For experienced users who want complete control over the desired
- capabilities, use this alternative constructor along with the
- C<desired_capabilities> hash key in the init hash. Unlike "new",
- this constructor will not assume any defaults for your desired
- capabilities.
- This alternate constructor IGNORES all other browser-related
- desiredCapability options; the only options that will be respected
- are those that are NOT part of the Capabilities JSON Object as
- described in the Json Wire Protocol.
- Input:
- The only respected keys in the input hash are:
- desired_capabilities - HASHREF - defaults to {}
- remote_server_addr - STRING - defaults to localhost
- port - INTEGER - defaults to 4444
- default_finder - STRING - defaults to xpath
- webelement_class - STRING - defaults to Selenium::Remote::WebElement
- auto_close - BOOLEAN - defaults to 1
- error_handler - CODEREF - defaults to croaking on exceptions
- Except for C<desired_capabilities>, these keys perform exactly the
- same as listed in the regular "new" constructor.
- The hashref you pass in as desired_capabilities only gets json
- encoded before being passed to the Selenium server; no default
- options of any sort will be added.
- This means you must handle normalization and casing of the input
- options (like "browser_name" vs "browserName") and take care of
- things like encoding the firefox profile if applicable. More
- information about the desired capabilities object is available on
- the Selenium wiki:
- https://code.google.com/p/selenium/wiki/JsonWireProtocol#Capabilities_JSON_Object
- Output:
- Remote Driver object
- Usage:
- my $driver = Selenium::Remote::Driver->new_from_caps(
- 'desired_capabilities' => {'browserName' => 'firefox'}
- );
- The above would generate a POST to the webdriver server at
- localhost:4444 with the exact payload of '{"desiredCapabilities":
- {"browserName": "firefox" }}'.
- =for Pod::Coverage has_base_url
- =for Pod::Coverage has_desired_capabilities
- =for Pod::Coverage has_error_handler
- =for Pod::Coverage has_firefox_profile
- =for Pod::Coverage has_inner_window_size
- =for Pod::Coverage has_javascript
- =for Pod::Coverage has_port
- =for Pod::Coverage has_remote_server_addr
- =cut
- sub new {
- my ( $class, %opts ) = @_;
- my $conn = Selenium::Remote::RemoteConnection->new(
- remote_server_addr => $opts{remote_server_addr},
- port => $opts{port},
- );
- $conn->check_status();
- if( $opts{'force_version'} eq '4' || ( $conn->{'version'} && $conn->{'version'} == 4 ) ) {
- require Selenium::Remote::Driver::v4;
- return Selenium::Remote::Driver::v4->new(
- 'port' => $opts{port},
- 'host' => $opts{remote_server_addr},
- 'browser' => $opts{browser_name},
- 'debug' => $opts{debug},
- );
- }
- require Selenium::Remote::Driver::v3;
- return Selenium::Remote::Driver::v3->new(%opts);
- }
- 1;
|