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 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, L, L, L,and L for details. If you'd like additional browsers besides these, give us a holler over in L. =head2 Remote Driver Response Selenium::Remote::Driver uses the L And the L 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. 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 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 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 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 or L 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 - - IP or FQDN of the Webdriver server machine. Default: 'localhost' =item B - - Port on which the Webdriver server is listening. Default: 4444 =item B - - desired browser string: {phantomjs|firefox|internet explorer|MicrosoftEdge|safari|htmlunit|iphone|chrome} =item B - - desired browser version number =item B - - desired platform: {WINDOWS|XP|VISTA|MAC|LINUX|UNIX|ANY} =item B - - whether SSL certs should be accepted, default is true. =item B - Profile - Use Selenium::Firefox::Profile to create a Firefox profile for the browser to use. =item B - - Whether or not to use Javascript. You probably won't disable this, as you would be using L instead. Default: True =item B - - Whether to automatically close the browser session on the server when the object goes out of scope. Default: False. =item B - - Default method by which to evaluate selectors. Default: 'xpath' =item B - - Provide a Session ID to highjack a browser session on the remote server. Useful for micro-optimizers. Default: undef =item B - STRING - OPTIONAL, 'normal|eager|none'. default 'normal'. WebDriver3 only. =item B - 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 - 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 - - whether driver should end session on remote server on close. =item B - - 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 - - choose default finder used for find_element* {class|class_name|css|id|link|link_text|name|partial_link_text|tag_name|xpath} =item B - - An array ref [ height, width ] that the browser window should use as its initial size immediately after instantiation =item B - CODEREF - A CODEREF that we will call in event of any exceptions. See L for more details. =item B - - sub-class of Selenium::Remote::WebElement if you wish to use an alternate WebElement class. =item B - LWP::UserAgent instance - if you wish to use a specific $ua, like from Test::LWP::UserAgent =item B - HASH - Proxy configuration with the following keys: =over 4 =item B - - 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 - - REQUIRED if proxyType is 'pac', ignored otherwise. Expected format: http://hostname.com:1234/pacfile or file:///path/to/pacfile =item B - - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234 =item B - - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234 =item B - - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234 =item B - - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234. WebDriver 3 only. =item B - - OPTIONAL, ignored if proxyType is not 'manual'. WebDriver 3 only. =item B - - OPTIONAL, list of URLs to bypass the proxy for. WebDriver3 only. =item B - - 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 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: 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: Four methods will still croak on their own: L, L, L, and L. If these methods throw a Webdriver Exception, your error handler _will still be_ invoked inside an C, 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, 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 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, 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;