2016-03-07 19:52:19 -05:00
# frozen_string_literal: true
2018-01-08 15:23:54 -05:00
2018-06-21 16:13:53 -04:00
require 'uri'
require 'English'
2013-10-20 13:29:22 -04:00
2011-04-11 01:24:00 -04:00
class Capybara :: Selenium :: Driver < Capybara :: Driver :: Base
2011-02-22 09:53:14 -05:00
DEFAULT_OPTIONS = {
2018-01-09 17:05:50 -05:00
browser : :firefox ,
2016-10-28 16:08:20 -04:00
clear_local_storage : false ,
clear_session_storage : false
2018-01-08 15:23:54 -05:00
} . freeze
SPECIAL_OPTIONS = % i [ browser clear_local_storage clear_session_storage ] . freeze
2012-07-13 07:29:02 -04:00
attr_reader :app , :options
2009-11-07 10:30:16 -05:00
2018-04-16 15:05:25 -04:00
def self . load_selenium
require 'selenium-webdriver'
2018-07-10 17:18:39 -04:00
warn " Warning: You're using an unsupported version of selenium-webdriver, please upgrade. " if Gem . loaded_specs [ 'selenium-webdriver' ] . version < Gem :: Version . new ( '3.5.0' )
2018-08-20 19:46:31 -04:00
rescue LoadError = > err
raise err if err . message !~ / selenium-webdriver /
2018-09-24 12:43:46 -04:00
2018-04-27 14:01:47 -04:00
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. "
2018-04-16 15:05:25 -04:00
end
2010-08-30 03:19:44 -04:00
def browser
2018-08-24 14:32:55 -04:00
@browser || = begin
processed_options = options . reject { | key , _val | SPECIAL_OPTIONS . include? ( key ) }
Selenium :: WebDriver . for ( options [ :browser ] , processed_options ) . tap do | driver |
specialize_driver ( driver )
setup_exit_handler
2009-11-14 04:44:46 -05:00
end
end
2018-09-16 14:50:56 -04:00
@browser
2009-11-07 10:30:16 -05:00
end
2016-08-17 19:14:39 -04:00
def initialize ( app , ** options )
2018-04-16 15:05:25 -04:00
self . class . load_selenium
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
2018-08-17 16:57:12 -04:00
@frame_handles = Hash . new { | hash , handle | hash [ handle ] = [ ] }
2011-02-22 09:53:14 -05:00
@options = DEFAULT_OPTIONS . merge ( options )
2018-06-06 19:11:47 -04:00
@node_class = :: Capybara :: Selenium :: Node
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
2018-02-15 17:34:06 -05:00
browser . navigate . refresh
2017-07-04 18:14:55 -04:00
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 )
2018-06-06 19:11:47 -04:00
browser . find_elements ( :xpath , selector ) . map ( & method ( :build_node ) )
2013-02-19 12:03:26 -05:00
end
2013-02-24 12:54:39 -05:00
2013-02-19 12:03:26 -05:00
def find_css ( selector )
2018-06-06 19:11:47 -04:00
browser . find_elements ( :css , selector ) . map ( & method ( :build_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 )
2018-05-11 12:42:15 -04:00
browser . execute_script ( script , * native_args ( args ) )
2010-06-29 16:41:33 -04:00
end
2016-12-22 19:22:46 -05:00
def evaluate_script ( script , * args )
2018-07-20 03:07:30 -04: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
2018-05-11 12:42:15 -04:00
result = browser . execute_async_script ( script , * native_args ( args ) )
2017-10-20 18:18:00 -04:00
unwrap_script_result ( result )
end
2016-08-17 19:14:39 -04: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
2018-01-09 17:05:50 -05:00
return unless @browser
2016-03-30 17:55:29 -04:00
2018-01-09 17:05:50 -05:00
navigated = false
2018-06-06 13:51:25 -04:00
timer = Capybara :: Helpers . timer ( expire_in : 10 )
2018-01-09 17:05:50 -05:00
begin
unless navigated
# Only trigger a navigation if we haven't done it already, otherwise it
# can trigger an endless series of unload modals
2018-08-22 20:15:16 -04:00
clear_browser_state
2018-07-10 17:18:39 -04:00
@browser . navigate . to ( 'about:blank' )
2018-01-09 17:05:50 -05:00
end
navigated = true
# Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
2018-07-10 17:18:39 -04:00
until find_xpath ( '/html/body/*' ) . empty?
2018-06-06 13:51:25 -04:00
raise Capybara :: ExpectationNotMet , 'Timed out waiting for Selenium session reset' if timer . expired?
2018-09-24 12:43:46 -04:00
2018-01-09 17:05:50 -05:00
sleep 0 . 05
end
rescue Selenium :: WebDriver :: Error :: UnhandledAlertError , Selenium :: WebDriver :: Error :: UnexpectedAlertOpenError
# 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
sleep 0 . 25 # allow time for the modal to be handled
rescue modal_error
2018-08-22 20:15:16 -04:00
# The alert is now gone.
# If navigation has not occurred attempt again and accept alert
# since FF may have dismissed the alert at first attempt.
navigate_with_accept ( 'about:blank' ) if current_url != 'about:blank'
2011-04-10 13:25:39 -04:00
end
2018-01-09 17:05:50 -05:00
# try cleaning up the browser again
retry
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 )
2018-08-17 16:57:12 -04:00
handles = @frame_handles [ current_window_handle ]
2016-07-15 14:00:14 -04:00
case frame
when :top
2018-08-17 16:57:12 -04:00
handles . clear
2016-07-15 14:00:14 -04:00
browser . switch_to . default_content
when :parent
2018-08-17 16:57:12 -04:00
handles . pop
2018-09-11 14:15:07 -04:00
browser . switch_to . parent_frame
2016-07-15 14:00:14 -04:00
else
2018-08-17 16:57:12 -04:00
handles << frame . native
2016-07-15 14:00:14 -04:00
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
2018-06-21 16:13:53 -04:00
browser . manage . window . resize_to ( width , height )
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
2018-06-21 14:30:26 -04:00
def fullscreen_window ( handle )
within_given_window ( handle ) do
2018-06-21 16:13:53 -04:00
browser . manage . window . full_screen
2018-06-21 14:30:26 -04:00
end
end
2014-04-24 18:38:42 -04:00
def close_window ( handle )
2018-07-10 17:18:39 -04:00
raise ArgumentError , 'Not allowed to close the primary window' if handle == window_handles . first
2018-09-24 12:43:46 -04:00
2014-04-24 18:38:42 -04:00
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
2016-08-17 19:14:39 -04:00
def accept_modal ( _type , ** options )
2018-02-12 13:07:06 -05:00
yield if block_given?
modal = find_modal ( options )
2017-10-09 04:37:45 -04:00
2018-02-12 13:07:06 -05:00
modal . send_keys options [ :with ] if options [ :with ]
2017-10-09 04:37:45 -04:00
2018-02-12 13:07:06 -05:00
message = modal . text
modal . accept
message
2013-04-01 18:41:55 -04:00
end
2016-08-17 19:14:39 -04:00
def dismiss_modal ( _type , ** options )
2018-02-12 13:07:06 -05:00
yield if block_given?
modal = find_modal ( options )
message = modal . text
modal . dismiss
message
2013-04-01 18:41:55 -04:00
end
2011-05-13 08:22:50 -04:00
def quit
2018-05-10 16:20:23 -04:00
@browser & . quit
2018-05-14 17:30:34 -04:00
rescue Selenium :: WebDriver :: Error :: SessionNotCreatedError , Errno :: ECONNREFUSED # rubocop:disable Lint/HandleExceptions
2011-05-13 08:22:50 -04:00
# Browser must have already gone
2018-08-20 19:46:31 -04:00
rescue Selenium :: WebDriver :: Error :: UnknownError = > err
unless silenced_unknown_error_message? ( err . message ) # Most likely already gone
2016-11-23 14:53:57 -05:00
# probably already gone but not sure - so warn
2018-08-20 19:46:31 -04:00
warn " Ignoring Selenium UnknownError during driver quit: #{ err . message } "
2016-11-23 14:53:57 -05:00
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
2018-01-08 15:23:54 -05: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 ,
:: Selenium :: WebDriver :: Error :: ElementClickInterceptedError ,
:: Selenium :: WebDriver :: Error :: InvalidElementStateError ,
2018-04-02 13:08:29 -04:00
:: Selenium :: WebDriver :: Error :: ElementNotSelectableError ,
:: Selenium :: WebDriver :: Error :: ElementNotSelectableError ,
2018-04-05 12:18:55 -04:00
:: Selenium :: WebDriver :: Error :: NoSuchElementError , # IE
:: Selenium :: WebDriver :: Error :: InvalidArgumentError # IE
2017-08-01 13:30:23 -04:00
]
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
2018-06-06 19:11:47 -04:00
private
2018-05-11 12:42:15 -04:00
def native_args ( args )
args . map { | arg | arg . is_a? ( Capybara :: Selenium :: Node ) ? arg . native : arg }
end
2018-08-22 20:15:16 -04:00
def clear_browser_state
@browser . manage . delete_all_cookies
clear_storage
rescue Selenium :: WebDriver :: Error :: UnhandledError # rubocop:disable Lint/HandleExceptions
# delete_all_cookies fails when we've previously gone
# to about:blank, so we rescue this error and do nothing
# instead.
end
2017-11-13 16:04:47 -05:00
def clear_storage
2018-08-24 07:45:19 -04:00
clear_session_storage if options [ :clear_session_storage ]
clear_local_storage if options [ :clear_local_storage ]
end
def 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'
2017-11-13 16:04:47 -05:00
end
2018-08-24 07:45:19 -04:00
end
2018-05-14 17:30:34 -04:00
2018-08-24 07:45:19 -04:00
def 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'
2017-11-13 16:04:47 -05:00
end
end
2018-08-22 20:15:16 -04:00
def navigate_with_accept ( url )
@browser . navigate . to ( url )
sleep 0 . 1 # slight wait for alert
@browser . switch_to . alert . accept
rescue modal_error # rubocop:disable Lint/HandleExceptions
# alert now gone, should mean navigation happened
end
2017-11-03 16:31:35 -04:00
def modal_error
2018-06-22 17:53:28 -04:00
Selenium :: WebDriver :: Error :: NoSuchAlertError
2017-11-03 16:31:35 -04:00
end
2014-04-24 18:38:42 -04:00
def within_given_window ( handle )
2018-01-09 17:05:50 -05:00
original_handle = current_window_handle
2014-04-24 18:38:42 -04:00
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
2016-08-17 19:14:39 -04:00
def find_modal ( text : nil , ** options )
2014-06-03 15:18:14 -04:00
# 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 (
2018-01-09 17:05:50 -05:00
timeout : options . fetch ( :wait , session_options . default_max_wait_time ) || 0 ,
2018-01-08 15:23:54 -05:00
ignore : modal_error
)
2014-06-03 15:18:14 -04:00
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
2016-08-17 19:14:39 -04:00
regexp = text . is_a? ( Regexp ) ? text : Regexp . escape ( text . to_s )
2014-06-03 15:18:14 -04:00
alert . text . match ( regexp ) ? alert : nil
end
rescue Selenium :: WebDriver :: Error :: TimeOutError
2018-01-09 17:05:50 -05:00
raise Capybara :: ModalNotFound , " Unable to find modal dialog #{ " with #{ text } " if text } "
2014-06-03 15:18:14 -04:00
end
2013-04-01 18:41:55 -04:00
end
2016-11-23 14:53:57 -05:00
def silenced_unknown_error_message? ( msg )
2018-08-20 19:46:31 -04:00
silenced_unknown_error_messages . any? { | regex | msg =~ regex }
2016-11-23 14:53:57 -05:00
end
def silenced_unknown_error_messages
2018-01-08 15:23:54 -05:00
[ / Error communicating with the remote browser / ]
2016-11-23 14:53:57 -05:00
end
2017-01-28 17:34:44 -05:00
def unwrap_script_result ( arg )
case arg
when Array
2018-08-20 19:46:31 -04:00
arg . map { | arr | unwrap_script_result ( arr ) }
2017-01-28 17:34:44 -05:00
when Hash
2018-08-20 19:46:31 -04:00
arg . each { | key , value | arg [ key ] = unwrap_script_result ( value ) }
2017-01-28 17:34:44 -05:00
when Selenium :: WebDriver :: Element
2018-06-06 19:11:47 -04:00
build_node ( arg )
2017-01-28 17:34:44 -05:00
else
arg
end
end
2018-06-06 19:11:47 -04:00
def build_node ( native_node )
2018-06-21 16:13:53 -04:00
:: Capybara :: Selenium :: Node . new ( self , native_node )
2018-06-06 19:11:47 -04:00
end
2018-08-24 14:32:55 -04:00
def specialize_driver ( sel_driver )
case sel_driver . browser
when :chrome
extend ChromeDriver
when :firefox
2018-09-16 14:50:56 -04:00
require 'capybara/selenium/patches/pause_duration_fix' if sel_driver . capabilities [ 'moz:geckodriverVersion' ] & . start_with? ( '0.22.' )
2018-08-24 14:32:55 -04:00
extend MarionetteDriver if sel_driver . capabilities . is_a? ( :: Selenium :: WebDriver :: Remote :: W3C :: Capabilities )
end
end
def setup_exit_handler
main = Process . pid
at_exit do
# Store the exit status of the test run since it goes away after calling the at_exit proc...
@exit_status = $ERROR_INFO . status if $ERROR_INFO . is_a? ( SystemExit )
quit if Process . pid == main
exit @exit_status if @exit_status # Force exit with stored status
end
end
2009-11-07 10:30:16 -05:00
end
2018-06-21 16:13:53 -04:00
require 'capybara/selenium/driver_specializations/chrome_driver'
require 'capybara/selenium/driver_specializations/marionette_driver'