2016-03-07 19:52:19 -05:00
# frozen_string_literal: true
2013-10-20 13:29:22 -04:00
require " uri "
2011-04-11 01:24:00 -04:00
class Capybara :: Selenium :: Driver < Capybara :: Driver :: Base
2016-10-06 21:04:14 -04:00
2011-02-22 09:53:14 -05:00
DEFAULT_OPTIONS = {
2016-10-28 16:08:20 -04:00
:browser = > :firefox ,
clear_local_storage : false ,
clear_session_storage : false
2011-02-22 09:53:14 -05:00
}
2016-10-28 16:08:20 -04:00
SPECIAL_OPTIONS = [ :browser , :clear_local_storage , :clear_session_storage ]
2011-02-22 09:53:14 -05:00
2012-07-13 07:29:02 -04:00
attr_reader :app , :options
2009-11-07 10:30:16 -05:00
2010-08-30 03:19:44 -04:00
def browser
unless @browser
2017-05-03 14:43:42 -04:00
if firefox?
2017-05-08 15:28:24 -04:00
options [ :desired_capabilities ] || = { }
2016-07-26 17:18:06 -04:00
options [ :desired_capabilities ] . merge! ( { unexpectedAlertBehaviour : " ignore " } )
end
2017-05-04 14:16:27 -04:00
@processed_options = options . reject { | key , _val | SPECIAL_OPTIONS . include? ( key ) }
@browser = Selenium :: WebDriver . for ( options [ :browser ] , @processed_options )
2011-05-13 08:22:50 -04:00
2017-05-08 18:12:33 -04:00
@w3c = ( ( defined? ( Selenium :: WebDriver :: Remote :: W3CCapabilities ) && @browser . capabilities . is_a? ( Selenium :: WebDriver :: Remote :: W3CCapabilities ) ) ||
( defined? ( Selenium :: WebDriver :: Remote :: W3C :: Capabilities ) && @browser . capabilities . is_a? ( Selenium :: WebDriver :: Remote :: W3C :: Capabilities ) ) )
2011-05-13 08:22:50 -04:00
main = Process . pid
2009-11-14 04:44:46 -05:00
at_exit do
2011-08-22 04:15:03 -04:00
# Store the exit status of the test run since it goes away after calling the at_exit proc...
@exit_status = $! . status if $! . is_a? ( SystemExit )
2011-05-13 08:22:50 -04:00
quit if Process . pid == main
2011-08-22 04:15:03 -04:00
exit @exit_status if @exit_status # Force exit with stored status
2009-11-14 04:44:46 -05:00
end
end
2010-08-30 03:19:44 -04:00
@browser
2009-11-07 10:30:16 -05:00
end
2010-09-07 12:35:55 -04:00
def initialize ( app , options = { } )
2017-05-28 11:54:55 -04:00
@session = nil
2013-03-18 18:43:48 -04:00
begin
require 'selenium-webdriver'
2017-04-21 23:31:28 -04:00
# Fix for selenium-webdriver 3.4.0 which misnamed these
if ! defined? ( :: Selenium :: WebDriver :: Error :: ElementNotInteractableError )
:: Selenium :: WebDriver :: Error . const_set ( 'ElementNotInteractableError' , Class . new ( :: Selenium :: WebDriver :: Error :: WebDriverError ) )
end
if ! defined? ( :: Selenium :: WebDriver :: Error :: ElementClickInterceptedError )
:: Selenium :: WebDriver :: Error . const_set ( 'ElementClickInterceptedError' , Class . new ( :: Selenium :: WebDriver :: Error :: WebDriverError ) )
end
2013-03-18 18:43:48 -04:00
rescue LoadError = > e
if e . message =~ / selenium-webdriver /
raise LoadError , " Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler. "
else
raise e
end
end
2016-10-06 21:04:14 -04:00
2009-11-07 10:30:16 -05:00
@app = app
2012-01-08 09:01:46 -05:00
@browser = nil
2012-01-08 09:02:24 -05:00
@exit_status = nil
2013-02-22 15:12:09 -05:00
@frame_handles = { }
2011-02-22 09:53:14 -05:00
@options = DEFAULT_OPTIONS . merge ( options )
2009-11-07 10:30:16 -05:00
end
2009-11-14 04:44:46 -05:00
2009-11-07 10:30:16 -05:00
def visit ( path )
2012-07-13 07:29:02 -04:00
browser . navigate . to ( path )
2009-11-07 10:30:16 -05:00
end
2009-11-14 04:44:46 -05:00
2017-07-04 18:14:55 -04:00
def refresh
2017-07-10 12:06:21 -04:00
accept_modal ( nil , wait : 0 . 1 ) do
2017-07-04 18:14:55 -04:00
browser . navigate . refresh
end
rescue Capybara :: ModalNotFound
end
2013-10-05 14:23:51 -04:00
def go_back
browser . navigate . back
end
def go_forward
browser . navigate . forward
end
2012-09-09 21:05:17 -04:00
def html
2010-01-01 14:13:54 -05:00
browser . page_source
2009-11-07 10:30:16 -05:00
end
2009-11-14 04:44:46 -05:00
2013-02-06 14:36:55 -05:00
def title
browser . title
end
2013-02-24 12:54:39 -05:00
2009-12-16 09:16:52 -05:00
def current_url
2010-01-01 14:13:54 -05:00
browser . current_url
2009-12-16 09:16:52 -05:00
end
2013-02-19 12:03:26 -05:00
def find_xpath ( selector )
browser . find_elements ( :xpath , selector ) . map { | node | Capybara :: Selenium :: Node . new ( self , node ) }
end
2013-02-24 12:54:39 -05:00
2013-02-19 12:03:26 -05:00
def find_css ( selector )
browser . find_elements ( :css , selector ) . map { | node | Capybara :: Selenium :: Node . new ( self , node ) }
2009-11-07 10:30:16 -05:00
end
2013-02-24 12:54:39 -05:00
2009-12-12 15:46:08 -05:00
def wait? ; true ; end
2012-07-13 07:29:02 -04:00
def needs_server? ; true ; end
2009-11-07 10:30:16 -05:00
2016-12-22 19:22:46 -05:00
def execute_script ( script , * args )
2017-01-03 23:41:37 -05:00
browser . execute_script ( script , * args . map { | arg | arg . is_a? ( Capybara :: Selenium :: Node ) ? arg . native : arg } )
2010-06-29 16:41:33 -04:00
end
2016-12-22 19:22:46 -05:00
def evaluate_script ( script , * args )
2017-03-07 19:32:02 -05:00
result = execute_script ( " return #{ script } " , * args )
2017-01-28 17:34:44 -05:00
unwrap_script_result ( result )
2010-01-01 14:13:54 -05:00
end
2010-01-01 16:46:05 -05:00
2017-10-20 18:18:00 -04:00
def evaluate_async_script ( script , * args )
browser . manage . timeouts . script_timeout = Capybara . default_max_wait_time
result = browser . execute_async_script ( script , * args . map { | arg | arg . is_a? ( Capybara :: Selenium :: Node ) ? arg . native : arg } )
unwrap_script_result ( result )
end
2016-11-21 19:28:45 -05:00
def save_screenshot ( path , _options = { } )
2012-07-10 00:50:15 -04:00
browser . save_screenshot ( path )
end
2010-07-29 09:25:45 -04:00
def reset!
2010-09-07 12:35:09 -04:00
# Use instance variable directly so we avoid starting the browser just to reset the session
2011-04-07 11:08:32 -04:00
if @browser
2016-03-30 17:55:29 -04:00
navigated = false
start_time = Capybara :: Helpers . monotonic_time
2014-06-03 15:18:14 -04:00
begin
2016-03-30 17:55:29 -04:00
if ! navigated
# Only trigger a navigation if we haven't done it already, otherwise it
# can trigger an endless series of unload modals
begin
@browser . manage . delete_all_cookies
2016-10-28 16:08:20 -04:00
if options [ :clear_session_storage ]
if @browser . respond_to? :session_storage
@browser . session_storage . clear
else
warn " sessionStorage clear requested but is not available for this driver "
end
end
if options [ :clear_local_storage ]
if @browser . respond_to? :local_storage
@browser . local_storage . clear
else
warn " localStorage clear requested but is not available for this driver "
end
end
2016-03-30 17:55:29 -04:00
rescue Selenium :: WebDriver :: Error :: UnhandledError
# delete_all_cookies fails when we've previously gone
# to about:blank, so we rescue this error and do nothing
# instead.
end
@browser . navigate . to ( " about:blank " )
end
navigated = true
#Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
until find_xpath ( " /html/body/* " ) . empty? do
raise Capybara :: ExpectationNotMet . new ( 'Timed out waiting for Selenium session reset' ) if ( Capybara :: Helpers . monotonic_time - start_time ) > = 10
sleep 0 . 05
2014-06-03 15:18:14 -04:00
end
2017-05-24 19:31:48 -04:00
rescue Selenium :: WebDriver :: Error :: UnhandledAlertError , Selenium :: WebDriver :: Error :: UnexpectedAlertOpenError
2014-06-03 15:18:14 -04:00
# This error is thrown if an unhandled alert is on the page
# Firefox appears to automatically dismiss this alert, chrome does not
# We'll try to accept it
begin
@browser . switch_to . alert . accept
2016-03-30 17:55:29 -04:00
sleep 0 . 25 # allow time for the modal to be handled
2014-06-03 15:18:14 -04:00
rescue Selenium :: WebDriver :: Error :: NoAlertPresentError
# The alert is now gone - nothing to do
end
# try cleaning up the browser again
retry
2011-04-10 13:25:39 -04:00
end
2011-04-07 11:08:32 -04:00
end
2010-01-23 06:58:30 -05:00
end
2016-07-15 14:00:14 -04:00
def switch_to_frame ( frame )
case frame
when :top
@frame_handles [ browser . window_handle ] = [ ]
browser . switch_to . default_content
when :parent
# would love to use browser.switch_to.parent_frame here
# but it has an issue if the current frame is removed from within it
@frame_handles [ browser . window_handle ] . pop
browser . switch_to . default_content
@frame_handles [ browser . window_handle ] . each { | fh | browser . switch_to . frame ( fh ) }
else
@frame_handles [ browser . window_handle ] || = [ ]
@frame_handles [ browser . window_handle ] << frame . native
browser . switch_to . frame ( frame . native )
end
2010-01-12 14:40:10 -05:00
end
2010-04-28 19:03:13 -04:00
2014-04-08 17:28:16 -04:00
def current_window_handle
browser . window_handle
end
2014-04-24 18:38:42 -04:00
def window_size ( handle )
within_given_window ( handle ) do
size = browser . manage . window . size
[ size . width , size . height ]
end
2014-04-08 17:28:16 -04:00
end
2014-04-24 18:38:42 -04:00
def resize_window_to ( handle , width , height )
within_given_window ( handle ) do
2017-04-24 13:57:38 -04:00
# Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
if marionette? && ( window_size ( handle ) == [ width , height ] )
{ }
else
browser . manage . window . resize_to ( width , height )
end
2014-04-24 18:38:42 -04:00
end
2014-04-08 17:28:16 -04:00
end
2014-04-24 18:38:42 -04:00
def maximize_window ( handle )
within_given_window ( handle ) do
browser . manage . window . maximize
end
2014-05-28 15:34:31 -04:00
sleep 0 . 1 # work around for https://code.google.com/p/selenium/issues/detail?id=7405
2014-04-10 03:20:27 -04:00
end
2014-04-24 18:38:42 -04:00
def close_window ( handle )
within_given_window ( handle ) do
browser . close
end
2014-04-08 17:28:16 -04:00
end
def window_handles
browser . window_handles
end
def open_new_window
browser . execute_script ( 'window.open();' )
end
def switch_to_window ( handle )
browser . switch_to . window handle
end
def within_window ( locator )
handle = find_window ( locator )
browser . switch_to . window ( handle ) { yield }
2010-08-27 15:00:08 -04:00
end
2016-11-21 19:28:45 -05:00
def accept_modal ( _type , options = { } )
2017-05-03 14:43:42 -04:00
if headless_chrome?
2017-08-14 19:25:57 -04:00
raise ArgumentError , " Block that triggers the system modal is missing " unless block_given?
2017-10-10 12:49:51 -04:00
insert_modal_handlers ( true , options [ :with ] )
2017-08-14 19:25:57 -04:00
yield
2017-05-03 14:43:42 -04:00
find_headless_modal ( options )
else
yield if block_given?
modal = find_modal ( options )
2017-10-09 04:37:45 -04:00
2017-05-03 14:43:42 -04:00
modal . send_keys options [ :with ] if options [ :with ]
2017-10-09 04:37:45 -04:00
2017-05-03 14:43:42 -04:00
message = modal . text
modal . accept
message
end
2013-04-01 18:41:55 -04:00
end
2016-11-21 19:28:45 -05:00
def dismiss_modal ( _type , options = { } )
2017-05-03 14:43:42 -04:00
if headless_chrome?
2017-08-14 19:25:57 -04:00
raise ArgumentError , " Block that triggers the system modal is missing " unless block_given?
2017-10-10 12:49:51 -04:00
insert_modal_handlers ( false , options [ :with ] )
2017-08-14 19:25:57 -04:00
yield
2017-05-03 14:43:42 -04:00
find_headless_modal ( options )
else
yield if block_given?
modal = find_modal ( options )
message = modal . text
modal . dismiss
message
end
2013-04-01 18:41:55 -04:00
end
2011-05-13 08:22:50 -04:00
def quit
2013-05-23 07:06:05 -04:00
@browser . quit if @browser
2011-05-13 08:22:50 -04:00
rescue Errno :: ECONNREFUSED
# Browser must have already gone
2016-10-13 12:47:10 -04:00
rescue Selenium :: WebDriver :: Error :: UnknownError = > e
2016-11-23 14:53:57 -05:00
unless silenced_unknown_error_message? ( e . message ) # Most likely already gone
# probably already gone but not sure - so warn
warn " Ignoring Selenium UnknownError during driver quit: #{ e . message } "
end
2013-11-22 18:59:51 -05:00
ensure
@browser = nil
2011-05-13 08:22:50 -04:00
end
2011-07-13 09:39:17 -04:00
def invalid_element_errors
2017-04-21 23:31:28 -04:00
[ :: Selenium :: WebDriver :: Error :: StaleElementReferenceError ,
:: Selenium :: WebDriver :: Error :: UnhandledError ,
:: Selenium :: WebDriver :: Error :: ElementNotVisibleError ,
:: Selenium :: WebDriver :: Error :: InvalidSelectorError , # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
:: Selenium :: WebDriver :: Error :: ElementNotInteractableError ,
2017-08-01 13:30:23 -04:00
:: Selenium :: WebDriver :: Error :: ElementClickInterceptedError ,
:: Selenium :: WebDriver :: Error :: InvalidElementStateError ,
:: Selenium :: WebDriver :: Error :: ElementNotSelectableError ,
]
2011-07-13 09:39:17 -04:00
end
2013-02-24 12:54:39 -05:00
2014-04-08 17:28:16 -04:00
def no_such_window_error
Selenium :: WebDriver :: Error :: NoSuchWindowError
end
2014-04-24 18:38:42 -04:00
2017-01-02 20:17:35 -05:00
# @api private
2017-05-08 18:12:33 -04:00
def marionette?
firefox? && browser && @w3c
end
# @api private
def firefox?
browser_name == " firefox "
end
# @api private
def chrome?
browser_name == " chrome "
end
# @api private
def headless_chrome?
2017-06-14 14:29:27 -04:00
if chrome?
caps = @processed_options [ :desired_capabilities ]
chrome_options = caps [ :chrome_options ] || caps [ :chromeOptions ] || { }
args = chrome_options [ 'args' ] || chrome_options [ :args ] || [ ]
2017-08-01 14:29:12 -04:00
return args . include? ( " --headless " ) || args . include? ( " headless " )
2017-06-14 14:29:27 -04:00
end
return false
2017-05-08 18:12:33 -04:00
end
2017-06-14 14:29:27 -04:00
2017-05-08 18:12:33 -04:00
# @deprecated This method is being removed
def browser_initialized?
super && ! @browser . nil?
end
private
# @api private
def browser_name
options [ :browser ] . to_s
end
2017-01-02 20:17:35 -05:00
def find_window ( locator )
handles = browser . window_handles
return locator if handles . include? locator
original_handle = browser . window_handle
handles . each do | handle |
switch_to_window ( handle )
if ( locator == browser . execute_script ( " return window.name " ) ||
browser . title . include? ( locator ) ||
browser . current_url . include? ( locator ) )
switch_to_window ( original_handle )
return handle
end
end
raise Capybara :: ElementNotFound , " Could not find a window identified by #{ locator } "
end
2017-10-10 12:49:51 -04:00
def insert_modal_handlers ( accept , response_text )
2017-05-03 14:43:42 -04:00
script = <<-JS
if ( typeof window . capybara === 'undefined' ) {
window . capybara = {
modal_handlers : [ ] ,
current_modal_status : function ( ) {
return [ this . modal_handlers [ 0 ] . called , this . modal_handlers [ 0 ] . modal_text ] ;
} ,
add_handler : function ( handler ) {
this . modal_handlers . unshift ( handler ) ;
} ,
remove_handler : function ( handler ) {
window . alert = handler . alert ;
window . confirm = handler . confirm ;
window . prompt = handler . prompt ;
} ,
handler_called : function ( handler , str ) {
handler . called = true ;
handler . modal_text = str ;
this . remove_handler ( handler ) ;
}
} ;
} ;
var modal_handler = {
prompt : window . prompt ,
confirm : window . confirm ,
alert : window . alert ,
2017-06-09 14:28:47 -04:00
called : false
2017-05-03 14:43:42 -04:00
}
window . capybara . add_handler ( modal_handler ) ;
2017-10-09 04:37:45 -04:00
window . alert = window . confirm = function ( str = " " ) {
window . capybara . handler_called ( modal_handler , str . toString ( ) ) ;
2017-05-03 14:43:42 -04:00
return #{accept ? 'true' : 'false'};
2017-10-09 04:37:45 -04:00
}
window . prompt = function ( str = " " , default_text = " " ) {
window . capybara . handler_called ( modal_handler , str . toString ( ) ) ;
return #{accept ? (response_text.nil? ? "default_text" : "'#{response_text}'") : 'null'};
2017-05-03 14:43:42 -04:00
}
JS
execute_script script
end
2014-04-24 18:38:42 -04:00
def within_given_window ( handle )
original_handle = self . current_window_handle
if handle == original_handle
yield
else
switch_to_window ( handle )
result = yield
switch_to_window ( original_handle )
result
end
end
2013-04-01 18:41:55 -04:00
2014-06-03 15:18:14 -04:00
def find_modal ( options = { } )
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
# Actual wait time may be longer than specified
wait = Selenium :: WebDriver :: Wait . new (
2016-12-15 12:04:01 -05:00
timeout : options . fetch ( :wait , session_options . default_max_wait_time ) || 0 ,
2014-06-03 15:18:14 -04:00
ignore : Selenium :: WebDriver :: Error :: NoAlertPresentError )
begin
2015-02-06 10:13:41 -05:00
wait . until do
2014-06-03 15:18:14 -04:00
alert = @browser . switch_to . alert
regexp = options [ :text ] . is_a? ( Regexp ) ? options [ :text ] : Regexp . escape ( options [ :text ] . to_s )
alert . text . match ( regexp ) ? alert : nil
end
rescue Selenium :: WebDriver :: Error :: TimeOutError
raise Capybara :: ModalNotFound . new ( " Unable to find modal dialog #{ " with #{ options [ :text ] } " if options [ :text ] } " )
end
2013-04-01 18:41:55 -04:00
end
2017-05-03 14:43:42 -04:00
def find_headless_modal ( options = { } )
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
# Actual wait time may be longer than specified
wait = Selenium :: WebDriver :: Wait . new (
timeout : options . fetch ( :wait , session_options . default_max_wait_time ) || 0 ,
ignore : Selenium :: WebDriver :: Error :: NoAlertPresentError )
begin
wait . until do
2017-06-09 14:28:47 -04:00
called , alert_text = evaluate_script ( 'window.capybara && window.capybara.current_modal_status()' )
2017-05-03 14:43:42 -04:00
if called
2017-06-09 14:28:47 -04:00
execute_script ( 'window.capybara && window.capybara.modal_handlers.shift()' )
2017-05-03 14:43:42 -04:00
regexp = options [ :text ] . is_a? ( Regexp ) ? options [ :text ] : Regexp . escape ( options [ :text ] . to_s )
if alert_text . match ( regexp )
alert_text
else
raise Capybara :: ModalNotFound . new ( " Unable to find modal dialog #{ " with #{ options [ :text ] } " if options [ :text ] } " )
end
2017-06-09 14:28:47 -04:00
elsif called . nil?
# page changed so modal_handler data has gone away
warn " Can't verify modal text when page change occurs - ignoring " if options [ :text ]
" "
2017-05-03 14:43:42 -04:00
else
nil
end
end
rescue Selenium :: WebDriver :: Error :: TimeOutError
raise Capybara :: ModalNotFound . new ( " Unable to find modal dialog #{ " with #{ options [ :text ] } " if options [ :text ] } " )
end
end
2016-11-23 14:53:57 -05:00
def silenced_unknown_error_message? ( msg )
silenced_unknown_error_messages . any? { | r | msg =~ r }
end
def silenced_unknown_error_messages
[ / Error communicating with the remote browser / ]
end
2017-01-28 17:34:44 -05:00
def unwrap_script_result ( arg )
case arg
when Array
arg . map { | e | unwrap_script_result ( e ) }
when Hash
arg . each { | k , v | arg [ k ] = unwrap_script_result ( v ) }
when Selenium :: WebDriver :: Element
Capybara :: Selenium :: Node . new ( self , arg )
else
arg
end
end
2009-11-07 10:30:16 -05:00
end