2016-03-07 16:52:19 -08:00
# frozen_string_literal: true
2018-01-08 12:23:54 -08:00
2015-08-23 22:14:48 -07:00
require 'capybara/session/matchers'
2016-11-30 09:03:29 -08:00
require 'addressable/uri'
2015-08-23 22:14:48 -07:00
2009-11-26 23:47:58 +01:00
module Capybara
2010-07-11 17:26:08 +02:00
##
#
2019-05-27 06:41:27 +09:00
# The {Session} class represents a single user's interaction with the system. The {Session} can use
2010-07-14 23:59:23 +02:00
# any of the underlying drivers. A session can be initialized manually like this:
2010-07-11 17:26:08 +02:00
#
# session = Capybara::Session.new(:culerity, MyRackApp)
#
# The application given as the second argument is optional. When running Capybara against an external
# page, you might want to leave it out:
#
# session = Capybara::Session.new(:culerity)
# session.visit('http://www.google.com')
#
2019-05-27 06:41:27 +09:00
# When {Capybara.configure threadsafe} is `true` the sessions options will be initially set to the
2016-12-15 09:04:01 -08:00
# current values of the global options and a configuration block can be passed to the session initializer.
2019-05-27 06:41:27 +09:00
# For available options see {Capybara::SessionConfig::OPTIONS}:
2016-12-15 09:04:01 -08:00
#
# session = Capybara::Session.new(:driver, MyRackApp) do |config|
# config.app_host = "http://my_host.dev"
# end
#
2019-05-27 06:41:27 +09:00
# The {Session} provides a number of methods for controlling the navigation of the page, such as {#visit},
# {#current_path}, and so on. It also delegates a number of methods to a {Capybara::Document}, representing
2010-07-11 17:26:08 +02:00
# the current HTML document. This allows interaction:
#
2016-10-04 11:10:29 -07:00
# session.fill_in('q', with: 'Capybara')
2010-07-11 17:26:08 +02:00
# session.click_button('Search')
2013-11-14 09:43:36 -08:00
# expect(session).to have_content('Capybara')
2010-07-11 17:26:08 +02:00
#
2019-05-27 06:41:27 +09:00
# When using `capybara/dsl`, the {Session} is initialized automatically for you.
2010-07-11 17:26:08 +02:00
#
class Session
2015-08-23 22:14:48 -07:00
include Capybara :: SessionMatchers
2018-01-08 12:23:54 -08:00
NODE_METHODS = % i [
2018-12-17 10:09:53 -08:00
all first attach_file text check choose scroll_to scroll_by
2018-01-10 16:47:39 -08:00
click_link_or_button click_button click_link
2018-01-08 12:23:54 -08:00
fill_in find find_all find_button find_by_id find_field find_link
has_content? has_text? has_css? has_no_content? has_no_text?
2018-11-12 10:07:20 -08:00
has_no_css? has_no_xpath? has_xpath? select uncheck
2018-01-08 12:23:54 -08:00
has_link? has_no_link? has_button? has_no_button? has_field?
has_no_field? has_checked_field? has_unchecked_field?
has_no_table? has_table? unselect has_select? has_no_select?
has_selector? has_no_selector? click_on has_no_checked_field?
has_no_unchecked_field? query assert_selector assert_no_selector
2018-10-23 10:02:34 -07:00
assert_all_of_selectors assert_none_of_selectors assert_any_of_selectors
2018-01-08 12:23:54 -08:00
refute_selector assert_text assert_no_text
] . freeze
2014-02-16 20:13:58 +03:00
# @api private
2018-01-08 12:23:54 -08:00
DOCUMENT_METHODS = % i [
title assert_title assert_no_title has_title? has_no_title?
] . freeze
SESSION_METHODS = % i [
body html source current_url current_host current_path
execute_script evaluate_script visit refresh go_back go_forward
within within_element within_fieldset within_table within_frame switch_to_frame
current_window windows open_new_window switch_to_window within_window window_opened_by
save_page save_and_open_page save_screenshot
save_and_open_screenshot reset_session! response_headers
status_code current_scope
assert_current_path assert_no_current_path has_current_path? has_no_current_path?
] . freeze + DOCUMENT_METHODS
MODAL_METHODS = % i [
accept_alert accept_confirm dismiss_confirm accept_prompt dismiss_prompt
] . freeze
2013-04-01 16:41:55 -06:00
DSL_METHODS = NODE_METHODS + SESSION_METHODS + MODAL_METHODS
2010-01-11 23:14:52 +01:00
2012-07-13 14:57:43 +02:00
attr_reader :mode , :app , :server
2013-03-12 00:11:05 +01:00
attr_accessor :synchronized
2009-11-09 23:51:39 +01:00
2018-01-09 14:05:50 -08:00
def initialize ( mode , app = nil )
2019-10-15 18:02:35 -07:00
if app && ! app . respond_to? ( :call )
raise TypeError , 'The second parameter to Session::new should be a rack app if passed.'
end
2018-09-24 09:43:46 -07:00
2019-05-08 16:03:57 -07:00
@@instance_created = true # rubocop:disable Style/ClassVars
2009-12-18 11:40:51 -05:00
@mode = mode
2009-11-26 23:47:58 +01:00
@app = app
2016-12-15 09:04:01 -08:00
if block_given?
2018-07-10 14:18:39 -07:00
raise 'A configuration block is only accepted when Capybara.threadsafe == true' unless Capybara . threadsafe
2018-09-24 09:43:46 -07:00
2018-01-13 13:06:03 -08:00
yield config
2016-12-15 09:04:01 -08:00
end
2018-05-14 14:30:34 -07:00
@server = if config . run_server && @app && driver . needs_server?
2018-05-16 20:04:24 -07:00
server_options = { port : config . server_port , host : config . server_host , reportable_errors : config . server_errors }
server_options [ :extra_middleware ] = [ Capybara :: Server :: AnimationDisabler ] if config . disable_animation
2019-11-28 14:41:13 -08:00
Capybara :: Server . new ( @app , ** server_options ) . boot
2012-07-13 13:29:02 +02:00
end
2012-12-02 15:30:29 +09:00
@touched = false
2009-11-16 20:13:06 +01:00
end
2010-01-01 17:48:39 +01:00
2009-11-26 23:47:58 +01:00
def driver
2010-07-29 15:20:11 +02:00
@driver || = begin
2018-01-09 14:05:50 -08:00
unless Capybara . drivers . key? ( mode )
other_drivers = Capybara . drivers . keys . map ( & :inspect )
2010-07-29 15:20:11 +02:00
raise Capybara :: DriverNotFoundError , " no driver called #{ mode . inspect } was found, available drivers: #{ other_drivers . join ( ', ' ) } "
end
2016-12-15 09:04:01 -08:00
driver = Capybara . drivers [ mode ] . call ( app )
2017-04-29 09:58:03 -07:00
driver . session = self if driver . respond_to? ( :session = )
2016-12-15 09:04:01 -08:00
driver
2009-11-26 23:47:58 +01:00
end
2009-11-15 13:40:50 +01:00
end
2009-11-26 23:47:58 +01:00
2010-07-11 17:26:08 +02:00
##
#
2019-05-27 06:41:27 +09:00
# Reset the session (i.e. remove cookies and navigate to blank page).
2014-04-03 10:57:31 +03:00
#
# This method does not:
#
2019-05-27 06:41:27 +09:00
# * accept modal dialogs if they are present (Selenium driver now does, others may not)
# * clear browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc.
# * modify state of the driver/underlying browser in any other way
2014-04-03 10:57:31 +03:00
#
# as doing so will result in performance downsides and it's not needed to do everything from the list above for most apps.
#
# If you want to do anything from the list above on a general basis you can:
#
2019-05-27 06:41:27 +09:00
# * write RSpec/Cucumber/etc. after hook
# * monkeypatch this method
# * use Ruby's `prepend` method
2010-07-11 17:26:08 +02:00
#
2010-07-29 15:25:45 +02:00
def reset!
2013-10-20 19:29:22 +02:00
if @touched
driver . reset!
2014-01-16 23:57:06 -08:00
@touched = false
2013-10-20 19:29:22 +02:00
end
2018-05-10 13:20:23 -07:00
@server & . wait_for_pending_requests
2014-03-02 15:49:00 +01:00
raise_server_error!
end
alias_method :cleanup! , :reset!
alias_method :reset_session! , :reset!
2018-11-28 16:21:29 -08:00
##
#
2019-05-27 06:41:27 +09:00
# Disconnect from the current driver. A new driver will be instantiated on the next interaction.
2018-11-28 16:21:29 -08:00
#
def quit
@driver . quit if @driver . respond_to? :quit
2019-04-10 14:44:02 -07:00
@document = @driver = nil
2018-11-28 16:21:29 -08:00
@touched = false
@server & . reset_error!
end
2014-03-02 15:49:00 +01:00
##
#
2019-05-27 06:41:27 +09:00
# Raise errors encountered in the server.
2014-03-02 15:49:00 +01:00
#
def raise_server_error!
2018-08-17 13:57:12 -07:00
return unless @server & . error
2018-09-24 09:43:46 -07:00
2018-04-27 11:01:47 -07:00
# Force an explanation for the error being raised as the exception cause
begin
if config . raise_server_errors
2018-07-10 14:18:39 -07:00
raise CapybaraError , 'Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true'
2016-07-13 11:11:11 -07:00
end
2018-04-27 11:01:47 -07:00
rescue CapybaraError
# needed to get the cause set correctly in JRuby -- otherwise we could just do raise @server.error
raise @server . error , @server . error . message , @server . error . backtrace
ensure
@server . reset_error!
2016-07-13 11:11:11 -07:00
end
2010-07-11 14:00:00 +02:00
end
2009-11-26 23:47:58 +01:00
2010-07-11 17:26:08 +02:00
##
#
2019-05-27 06:41:27 +09:00
# Returns a hash of response headers. Not supported by all drivers (e.g. Selenium).
2010-07-11 17:26:08 +02:00
#
2019-05-27 06:41:27 +09:00
# @return [Hash<String, String>] A hash of response headers.
2010-07-11 17:26:08 +02:00
#
2010-07-11 14:00:00 +02:00
def response_headers
driver . response_headers
2009-12-13 15:03:19 +01:00
end
2010-07-11 17:26:08 +02:00
##
#
2019-05-27 06:41:27 +09:00
# Returns the current HTTP status code as an integer. Not supported by all drivers (e.g. Selenium).
2010-07-11 17:26:08 +02:00
#
2010-07-15 00:24:59 +02:00
# @return [Integer] Current HTTP status code
2010-07-11 17:26:08 +02:00
#
2010-07-11 14:00:00 +02:00
def status_code
driver . status_code
2009-11-24 21:45:52 +01:00
end
2009-11-07 18:56:04 +01:00
2010-07-11 17:26:08 +02:00
##
#
2012-09-10 03:05:17 +02:00
# @return [String] A snapshot of the DOM of the current document, as it looks right now (potentially modified by JavaScript).
2010-07-11 17:26:08 +02:00
#
2011-11-15 16:58:05 +01:00
def html
2012-09-10 03:05:17 +02:00
driver . html
2010-07-11 14:00:00 +02:00
end
2012-11-20 14:22:19 +01:00
alias_method :body , :html
alias_method :source , :html
2010-07-11 14:00:00 +02:00
2010-07-11 17:26:08 +02:00
##
#
2010-07-15 00:24:59 +02:00
# @return [String] Path of the current page, without any domain information
2010-07-11 17:26:08 +02:00
#
2010-07-10 03:11:54 +02:00
def current_path
2016-11-30 09:03:29 -08:00
# Addressable parsing is more lenient than URI
2017-01-26 16:10:51 -08:00
uri = :: Addressable :: URI . parse ( current_url )
2017-01-12 17:55:26 -08:00
2016-11-30 09:03:29 -08:00
# Addressable doesn't support opaque URIs - we want nil here
2018-07-10 14:18:39 -07:00
return nil if uri & . scheme == 'about'
2018-01-13 13:06:03 -08:00
2018-05-15 21:43:45 -07:00
path = uri & . path
2018-05-16 12:47:08 -07:00
path unless path & . empty?
2010-07-10 03:11:54 +02:00
end
2011-03-11 17:01:13 +01:00
##
#
# @return [String] Host of the current page
#
def current_host
2011-03-25 09:01:59 +00:00
uri = URI . parse ( current_url )
2011-04-07 16:18:11 +02:00
" #{ uri . scheme } :// #{ uri . host } " if uri . host
2011-03-11 17:01:13 +01:00
end
2010-07-11 17:26:08 +02:00
##
#
2010-07-15 00:24:59 +02:00
# @return [String] Fully qualified URL of the current page
2010-07-11 17:26:08 +02:00
#
def current_url
driver . current_url
end
2013-02-16 09:34:30 +01:00
2010-07-11 17:26:08 +02:00
##
#
# Navigate to the given URL. The URL can either be a relative URL or an absolute URL
# The behaviour of either depends on the driver.
#
# session.visit('/foo')
# session.visit('http://google.com')
#
2012-08-02 17:31:50 +02:00
# For drivers which can run against an external application, such as the selenium driver
2010-07-11 17:26:08 +02:00
# giving an absolute URL will navigate to that page. This allows testing applications
2019-05-27 06:41:27 +09:00
# running on remote servers. For these drivers, setting {Capybara.configure app_host} will make the
2010-07-11 17:26:08 +02:00
# remote server the default. For example:
#
# Capybara.app_host = 'http://google.com'
# session.visit('/') # visits the google homepage
#
2019-05-27 06:41:27 +09:00
# If {Capybara.configure always_include_port} is set to `true` and this session is running against
2012-08-02 17:31:50 +02:00
# a rack application, then the port that the rack application is running on will automatically
# be inserted into the URL. Supposing the app is running on port `4567`, doing something like:
#
# visit("http://google.com/test")
#
# Will actually navigate to `http://google.com:4567/test`.
#
2015-09-18 12:46:33 -07:00
# @param [#to_s] visit_uri The URL to navigate to. The parameter will be cast to a String.
2010-07-11 17:26:08 +02:00
#
2014-06-30 14:44:33 +09:00
def visit ( visit_uri )
2014-03-02 15:49:00 +01:00
raise_server_error!
2012-07-10 10:03:05 +02:00
@touched = true
2012-07-13 13:29:02 +02:00
2017-08-04 16:42:54 -07:00
visit_uri = :: Addressable :: URI . parse ( visit_uri . to_s )
2018-08-24 06:44:21 -07:00
base_uri = :: Addressable :: URI . parse ( config . app_host || server_url )
2014-06-19 20:20:50 +03:00
2018-08-24 06:44:21 -07:00
if base_uri && [ nil , 'http' , 'https' ] . include? ( visit_uri . scheme )
2017-10-06 11:32:10 -07:00
if visit_uri . relative?
2019-03-25 09:37:25 -07:00
visit_uri_parts = visit_uri . to_hash . compact
2014-06-30 14:44:33 +09:00
2017-11-19 10:06:18 -08:00
# Useful to people deploying to a subdirectory
# and/or single page apps where only the url fragment changes
2018-08-24 06:44:21 -07:00
visit_uri_parts [ :path ] = base_uri . path + visit_uri . path
2017-10-03 10:18:03 -07:00
2018-08-24 06:44:21 -07:00
visit_uri = base_uri . merge ( visit_uri_parts )
2017-10-06 11:32:10 -07:00
end
2018-08-24 06:44:21 -07:00
adjust_server_port ( visit_uri )
2017-10-03 10:18:03 -07:00
end
2017-10-06 11:32:10 -07:00
2014-06-30 14:44:33 +09:00
driver . visit ( visit_uri . to_s )
2010-07-11 14:00:00 +02:00
end
2017-07-04 15:14:55 -07:00
##
#
2019-05-27 06:41:27 +09:00
# Refresh the page.
2017-07-04 15:14:55 -07:00
#
def refresh
raise_server_error!
driver . refresh
end
2013-10-05 21:23:51 +03:00
##
#
# Move back a single entry in the browser's history.
#
def go_back
driver . go_back
end
##
#
# Move forward a single entry in the browser's history.
#
def go_forward
driver . go_forward
end
2010-07-11 17:26:08 +02:00
##
#
2019-05-27 06:41:27 +09:00
# Executes the given block within the context of a node. {#within} takes the
# same options as {Capybara::Node::Finders#find #find}, as well as a block. For the duration of the
2013-08-26 15:13:30 +02:00
# block, any command to Capybara will be handled as though it were scoped
# to the given element.
2010-07-11 17:26:08 +02:00
#
2016-10-26 12:24:51 -07:00
# within(:xpath, './/div[@id="delivery-address"]') do
2016-10-04 11:10:29 -07:00
# fill_in('Street', with: '12 Main Street')
2010-07-11 17:26:08 +02:00
# end
#
2019-05-27 06:41:27 +09:00
# Just as with `#find`, if multiple elements match the selector given to
# {#within}, an error will be raised, and just as with `#find`, this
2013-08-26 15:13:30 +02:00
# behaviour can be controlled through the `:match` and `:exact` options.
#
# It is possible to omit the first parameter, in that case, the selector is
2019-05-27 06:41:27 +09:00
# assumed to be of the type set in {Capybara.configure default_selector}.
2010-07-11 17:26:08 +02:00
#
# within('div#delivery-address') do
2016-10-04 11:10:29 -07:00
# fill_in('Street', with: '12 Main Street')
2010-07-11 17:26:08 +02:00
# end
#
2019-05-27 06:41:27 +09:00
# Note that a lot of uses of {#within} can be replaced more succinctly with
2013-08-26 15:13:30 +02:00
# chaining:
#
2016-10-04 11:10:29 -07:00
# find('div#delivery-address').fill_in('Street', with: '12 Main Street')
2013-08-26 15:13:30 +02:00
#
2011-08-26 11:36:51 -06:00
# @overload within(*find_args)
# @param (see Capybara::Node::Finders#all)
#
# @overload within(a_node)
# @param [Capybara::Node::Base] a_node The node in whose scope the block should be evaluated
#
2013-08-26 15:13:30 +02:00
# @raise [Capybara::ElementNotFound] If the scope can't be found before time expires
2010-07-11 17:26:08 +02:00
#
2010-12-22 16:15:06 +01:00
def within ( * args )
2018-02-13 11:14:59 -08:00
new_scope = args . first . respond_to? ( :to_capybara_node ) ? args . first . to_capybara_node : find ( * args )
2010-02-11 12:46:31 -08:00
begin
2010-07-10 01:38:57 +02:00
scopes . push ( new_scope )
2018-05-29 13:26:24 -07:00
yield if block_given?
2010-02-11 12:46:31 -08:00
ensure
scopes . pop
end
2009-11-26 23:47:58 +01:00
end
2017-01-18 08:47:48 -08:00
alias_method :within_element , :within
2009-11-14 15:11:29 +01:00
2010-07-11 17:26:08 +02:00
##
#
# Execute the given block within the a specific fieldset given the id or legend of that fieldset.
#
2010-07-15 00:24:59 +02:00
# @param [String] locator Id or legend of the fieldset
2010-07-11 17:26:08 +02:00
#
2009-11-26 23:47:58 +01:00
def within_fieldset ( locator )
2018-01-13 13:06:03 -08:00
within ( :fieldset , locator ) { yield }
2009-11-10 22:48:31 +01:00
end
2010-07-11 17:26:08 +02:00
##
#
# Execute the given block within the a specific table given the id or caption of that table.
#
2010-07-15 00:24:59 +02:00
# @param [String] locator Id or caption of the table
2010-07-11 17:26:08 +02:00
#
2009-11-26 23:47:58 +01:00
def within_table ( locator )
2018-01-13 13:06:03 -08:00
within ( :table , locator ) { yield }
2009-11-26 23:47:58 +01:00
end
2010-01-01 18:29:30 +01:00
2016-12-08 16:57:45 -08:00
##
#
2019-05-27 06:41:27 +09:00
# Switch to the given frame.
2016-12-08 16:57:45 -08:00
#
# If you use this method you are responsible for making sure you switch back to the parent frame when done in the frame changed to.
2019-05-27 06:41:27 +09:00
# {#within_frame} is preferred over this method and should be used when possible.
2016-12-08 16:57:45 -08:00
# May not be supported by all drivers.
#
# @overload switch_to_frame(element)
2019-05-27 06:41:27 +09:00
# @param [Capybara::Node::Element] element iframe/frame element to switch to
# @overload switch_to_frame(location)
# @param [Symbol] location relative location of the frame to switch to
# * :parent - the parent frame
# * :top - the top level document
2016-12-08 16:57:45 -08:00
#
def switch_to_frame ( frame )
case frame
when Capybara :: Node :: Element
driver . switch_to_frame ( frame )
scopes . push ( :frame )
when :parent
2018-01-09 14:05:50 -08:00
if scopes . last != :frame
raise Capybara :: ScopeError , " `switch_to_frame(:parent)` cannot be called from inside a descendant frame's " \
2018-07-10 14:18:39 -07:00
'`within` block.'
2018-01-09 14:05:50 -08:00
end
2016-12-08 16:57:45 -08:00
scopes . pop
driver . switch_to_frame ( :parent )
when :top
idx = scopes . index ( :frame )
if idx
2018-01-09 14:05:50 -08:00
if scopes . slice ( idx .. - 1 ) . any? { | scope | ! [ :frame , nil ] . include? ( scope ) }
raise Capybara :: ScopeError , " `switch_to_frame(:top)` cannot be called from inside a descendant frame's " \
2018-07-10 14:18:39 -07:00
'`within` block.'
2018-01-09 14:05:50 -08:00
end
2016-12-08 16:57:45 -08:00
scopes . slice! ( idx .. - 1 )
driver . switch_to_frame ( :top )
end
2018-07-06 08:56:40 -07:00
else
2018-07-10 14:18:39 -07:00
raise ArgumentError , 'You must provide a frame element, :parent, or :top when calling switch_to_frame'
2016-12-08 16:57:45 -08:00
end
end
2010-07-11 17:26:08 +02:00
##
#
2016-07-15 11:00:14 -07:00
# Execute the given block within the given iframe using given frame, frame name/id or index.
# May not be supported by all drivers.
2010-07-11 17:26:08 +02:00
#
2016-07-15 11:00:14 -07:00
# @overload within_frame(element)
# @param [Capybara::Node::Element] frame element
2018-05-17 14:45:53 -07:00
# @overload within_frame([kind = :frame], locator, **options)
2018-07-16 16:42:10 -07:00
# @param [Symbol] kind Optional selector type (:frame, :css, :xpath, etc.) - Defaults to :frame
2016-12-08 11:17:21 -08:00
# @param [String] locator The locator for the given selector kind. For :frame this is the name/id of a frame/iframe element
2013-01-08 11:40:12 +03:00
# @overload within_frame(index)
2016-12-08 11:17:21 -08:00
# @param [Integer] index index of a frame (0 based)
def within_frame ( * args )
2016-08-17 14:07:44 -07:00
switch_to_frame ( _find_frame ( * args ) )
2016-07-15 11:00:14 -07:00
begin
2018-05-29 13:26:24 -07:00
yield if block_given?
2016-08-17 14:07:44 -07:00
ensure
switch_to_frame ( :parent )
2009-11-26 23:47:58 +01:00
end
2010-08-27 21:00:08 +02:00
end
##
2014-04-09 00:28:16 +03:00
# @return [Capybara::Window] current window
2010-08-27 21:00:08 +02:00
#
2014-04-09 00:28:16 +03:00
def current_window
Window . new ( self , driver . current_window_handle )
end
##
# Get all opened windows.
# The order of windows in returned array is not defined.
# The driver may sort windows by their creation time but it's not required.
2010-08-27 21:00:08 +02:00
#
2014-04-09 00:28:16 +03:00
# @return [Array<Capybara::Window>] an array of all windows
2010-08-27 21:00:08 +02:00
#
2014-04-09 00:28:16 +03:00
def windows
driver . window_handles . map do | handle |
Window . new ( self , handle )
end
end
##
2019-05-27 06:41:27 +09:00
# Open a new window.
# The current window doesn't change as the result of this call.
2014-04-09 00:28:16 +03:00
# It should be switched to explicitly.
#
# @return [Capybara::Window] window that has been opened
#
2019-03-26 12:05:21 -07:00
def open_new_window ( kind = :tab )
2014-04-09 00:28:16 +03:00
window_opened_by do
2019-03-26 12:05:21 -07:00
if driver . method ( :open_new_window ) . arity . zero?
driver . open_new_window
else
driver . open_new_window ( kind )
end
2014-04-09 00:28:16 +03:00
end
end
##
2019-05-27 06:41:27 +09:00
# Switch to the given window.
#
2014-04-09 00:28:16 +03:00
# @overload switch_to_window(&block)
# Switches to the first window for which given block returns a value other than false or nil.
2019-05-27 06:41:27 +09:00
# If window that matches block can't be found, the window will be switched back and {Capybara::WindowError} will be raised.
2014-04-09 00:28:16 +03:00
# @example
# window = switch_to_window { title == 'Page title' }
# @raise [Capybara::WindowError] if no window matches given block
# @overload switch_to_window(window)
# @param window [Capybara::Window] window that should be switched to
2017-12-26 15:44:52 -07:00
# @raise [Capybara::Driver::Base#no_such_window_error] if nonexistent (e.g. closed) window was passed
2014-04-09 00:28:16 +03:00
#
2014-04-09 11:22:17 +03:00
# @return [Capybara::Window] window that has been switched to
2019-05-27 06:41:27 +09:00
# @raise [Capybara::ScopeError] if this method is invoked inside {#within} or
# {#within_frame} methods
2014-04-09 00:28:16 +03:00
# @raise [ArgumentError] if both or neither arguments were provided
#
2016-08-17 16:14:39 -07:00
def switch_to_window ( window = nil , ** options , & window_locator )
2018-07-10 14:18:39 -07:00
raise ArgumentError , '`switch_to_window` can take either a block or a window, not both' if window && block_given?
raise ArgumentError , '`switch_to_window`: either window or block should be provided' if ! window && ! block_given?
2018-09-24 09:43:46 -07:00
2018-01-09 14:05:50 -08:00
unless scopes . last . nil?
2018-07-10 14:18:39 -07:00
raise Capybara :: ScopeError , '`switch_to_window` is not supposed to be invoked from ' \
'`within` or `within_frame` blocks.'
2014-04-09 00:28:16 +03:00
end
2019-11-28 14:41:13 -08:00
_switch_to_window ( window , ** options , & window_locator )
2014-04-09 00:28:16 +03:00
end
##
# This method does the following:
#
# 1. Switches to the given window (it can be located by window instance/lambda/string).
# 2. Executes the given block (within window located at previous step).
2019-05-27 06:41:27 +09:00
# 3. Switches back (this step will be invoked even if an exception occurs at the second step).
2014-04-09 00:28:16 +03:00
#
# @overload within_window(window) { do_something }
2019-05-27 06:41:27 +09:00
# @param window [Capybara::Window] instance of {Capybara::Window} class
2014-04-09 00:28:16 +03:00
# that will be switched to
2017-12-26 15:44:52 -07:00
# @raise [driver#no_such_window_error] if nonexistent (e.g. closed) window was passed
2014-04-09 00:28:16 +03:00
# @overload within_window(proc_or_lambda) { do_something }
2019-05-27 06:41:27 +09:00
# @param lambda [Proc] First window for which lambda
2014-04-09 00:28:16 +03:00
# returns a value other than false or nil will be switched to.
# @example
# within_window(->{ page.title == 'Page title' }) { click_button 'Submit' }
# @raise [Capybara::WindowError] if no window matching lambda was found
#
2019-05-27 06:41:27 +09:00
# @raise [Capybara::ScopeError] if this method is invoked inside {#within_frame} method
2014-04-09 00:28:16 +03:00
# @return value returned by the block
#
2018-01-11 16:45:50 -08:00
def within_window ( window_or_proc )
original = current_window
scopes << nil
begin
case window_or_proc
when Capybara :: Window
_switch_to_window ( window_or_proc ) unless original == window_or_proc
when Proc
_switch_to_window { window_or_proc . call }
else
2019-10-03 16:39:08 +02:00
raise ArgumentError , '`#within_window` requires a `Capybara::Window` instance or a lambda'
2014-04-09 00:28:16 +03:00
end
2018-01-11 16:45:50 -08:00
2014-04-09 00:28:16 +03:00
begin
2018-05-29 13:26:24 -07:00
yield if block_given?
2014-04-09 00:28:16 +03:00
ensure
2018-01-11 16:45:50 -08:00
_switch_to_window ( original ) unless original == window_or_proc
2014-04-09 00:28:16 +03:00
end
2018-01-11 16:45:50 -08:00
ensure
scopes . pop
2014-04-09 00:28:16 +03:00
end
end
##
# Get the window that has been opened by the passed block.
# It will wait for it to be opened (in the same way as other Capybara methods wait).
# It's better to use this method than `windows.last`
2019-05-27 06:41:27 +09:00
# {https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10 as order of windows isn't defined in some drivers}.
2014-04-09 00:28:16 +03:00
#
2018-01-08 12:23:54 -08:00
# @overload window_opened_by(**options, &block)
# @param options [Hash]
2019-05-27 06:41:27 +09:00
# @option options [Numeric] :wait maximum wait time. Defaults to {Capybara.configure default_max_wait_time}
2018-01-08 12:23:54 -08:00
# @return [Capybara::Window] the window that has been opened within a block
# @raise [Capybara::WindowError] if block passed to window hasn't opened window
# or opened more than one window
2014-04-09 00:28:16 +03:00
#
2018-01-08 12:23:54 -08:00
def window_opened_by ( ** options )
2014-04-09 00:28:16 +03:00
old_handles = driver . window_handles
2018-01-08 12:23:54 -08:00
yield
2014-04-09 00:28:16 +03:00
2018-08-24 06:44:21 -07:00
synchronize_windows ( options ) do
2014-04-09 00:28:16 +03:00
opened_handles = ( driver . window_handles - old_handles )
if opened_handles . size != 1
2018-07-10 14:18:39 -07:00
raise Capybara :: WindowError , 'block passed to #window_opened_by ' \
2014-04-09 00:28:16 +03:00
" opened #{ opened_handles . size } windows instead of 1 "
end
Window . new ( self , opened_handles . first )
end
2009-11-26 23:47:58 +01:00
end
2010-01-01 18:29:30 +01:00
2010-07-11 17:26:08 +02:00
##
#
# Execute the given script, not returning a result. This is useful for scripts that return
2019-05-27 06:41:27 +09:00
# complex objects, such as jQuery statements. {#execute_script} should be used over
# {#evaluate_script} whenever possible.
2010-07-11 17:26:08 +02:00
#
2010-07-15 00:24:59 +02:00
# @param [String] script A string of JavaScript to execute
2019-05-27 06:41:27 +09:00
# @param args Optional arguments that will be passed to the script. Driver support for this is optional and types of objects supported may differ between drivers
2010-07-11 17:26:08 +02:00
#
2016-12-22 16:22:46 -08:00
def execute_script ( script , * args )
2012-07-10 10:03:05 +02:00
@touched = true
2018-05-15 21:43:45 -07:00
driver . execute_script ( script , * driver_args ( args ) )
2010-07-10 01:42:59 +02:00
end
2010-07-11 17:26:08 +02:00
##
#
# Evaluate the given JavaScript and return the result. Be careful when using this with
2019-05-27 06:41:27 +09:00
# scripts that return complex objects, such as jQuery statements. {#execute_script} might
2010-07-11 17:26:08 +02:00
# be a better alternative.
#
2010-07-15 00:24:59 +02:00
# @param [String] script A string of JavaScript to evaluate
2019-05-27 06:41:27 +09:00
# @param args Optional arguments that will be passed to the script
2010-07-15 00:24:59 +02:00
# @return [Object] The result of the evaluated JavaScript (may be driver specific)
2010-07-11 17:26:08 +02:00
#
2016-12-22 16:22:46 -08:00
def evaluate_script ( script , * args )
2012-07-10 10:03:05 +02:00
@touched = true
2018-07-20 00:07:30 -07:00
result = driver . evaluate_script ( script . strip , * driver_args ( args ) )
2017-01-28 14:34:44 -08:00
element_script_result ( result )
2010-07-10 01:42:59 +02:00
end
2017-10-20 15:18:00 -07:00
##
#
# Evaluate the given JavaScript and obtain the result from a callback function which will be passed as the last argument to the script.
#
# @param [String] script A string of JavaScript to evaluate
2019-05-27 06:41:27 +09:00
# @param args Optional arguments that will be passed to the script
2017-10-20 15:18:00 -07:00
# @return [Object] The result of the evaluated JavaScript (may be driver specific)
#
def evaluate_async_script ( script , * args )
@touched = true
2018-05-15 21:43:45 -07:00
result = driver . evaluate_async_script ( script , * driver_args ( args ) )
2017-10-20 15:18:00 -07:00
element_script_result ( result )
end
2013-04-01 16:41:55 -06:00
##
#
# Execute the block, accepting a alert.
#
2014-07-02 19:31:13 -07:00
# @!macro modal_params
2019-05-27 06:41:27 +09:00
# Expects a block whose actions will trigger the display modal to appear.
2017-08-14 16:23:45 -07:00
# @example
# $0 do
# click_link('link that triggers appearance of system modal')
# end
2018-05-17 14:45:53 -07:00
# @overload $0(text, **options, &blk)
2019-05-27 06:41:27 +09:00
# @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any modal is matched.
# @option options [Numeric] :wait Maximum time to wait for the modal to appear after executing the block. Defaults to {Capybara.configure default_max_wait_time}.
2017-08-14 16:23:45 -07:00
# @yield Block whose actions will trigger the system modal
2018-05-17 14:45:53 -07:00
# @overload $0(**options, &blk)
2019-05-27 06:41:27 +09:00
# @option options [Numeric] :wait Maximum time to wait for the modal to appear after executing the block. Defaults to {Capybara.configure default_max_wait_time}.
2017-08-14 16:23:45 -07:00
# @yield Block whose actions will trigger the system modal
2014-07-02 19:31:13 -07:00
# @return [String] the message shown in the modal
# @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
2014-06-03 12:18:14 -07:00
#
2018-01-09 14:05:50 -08:00
def accept_alert ( text = nil , ** options , & blk )
2016-08-17 16:14:39 -07:00
accept_modal ( :alert , text , options , & blk )
2013-04-01 16:41:55 -06:00
end
##
#
# Execute the block, accepting a confirm.
#
2014-07-02 19:31:13 -07:00
# @macro modal_params
2014-06-03 12:18:14 -07:00
#
2018-01-09 14:05:50 -08:00
def accept_confirm ( text = nil , ** options , & blk )
2016-08-17 16:14:39 -07:00
accept_modal ( :confirm , text , options , & blk )
2013-04-01 16:41:55 -06:00
end
##
#
# Execute the block, dismissing a confirm.
#
2014-07-02 19:31:13 -07:00
# @macro modal_params
2014-06-03 12:18:14 -07:00
#
2018-01-09 14:05:50 -08:00
def dismiss_confirm ( text = nil , ** options , & blk )
2016-08-17 16:14:39 -07:00
dismiss_modal ( :confirm , text , options , & blk )
2013-04-01 16:41:55 -06:00
end
##
#
2014-06-27 12:37:33 -07:00
# Execute the block, accepting a prompt, optionally responding to the prompt.
2013-04-01 16:41:55 -06:00
#
2014-07-02 19:31:13 -07:00
# @macro modal_params
2014-06-27 12:37:33 -07:00
# @option options [String] :with Response to provide to the prompt
2014-06-03 12:18:14 -07:00
#
2018-01-09 14:05:50 -08:00
def accept_prompt ( text = nil , ** options , & blk )
2016-08-17 16:14:39 -07:00
accept_modal ( :prompt , text , options , & blk )
2013-04-01 16:41:55 -06:00
end
##
#
# Execute the block, dismissing a prompt.
#
2014-07-02 19:31:13 -07:00
# @macro modal_params
2014-06-03 12:18:14 -07:00
#
2018-01-09 14:05:50 -08:00
def dismiss_prompt ( text = nil , ** options , & blk )
2016-08-17 16:14:39 -07:00
dismiss_modal ( :prompt , text , options , & blk )
2013-04-01 16:41:55 -06:00
end
2010-07-11 17:26:08 +02:00
##
#
2019-05-27 06:41:27 +09:00
# Save a snapshot of the page. If {Capybara.configure asset_host} is set it will inject `base` tag
# pointing to {Capybara.configure asset_host}.
2012-07-24 09:05:50 +02:00
#
2019-05-27 06:41:27 +09:00
# If invoked without arguments it will save file to {Capybara.configure save_path}
# and file will be given randomly generated filename. If invoked with a relative path
# the path will be relative to {Capybara.configure save_path}.
2010-07-11 17:26:08 +02:00
#
2014-08-24 14:48:20 +03:00
# @param [String] path the path to where it should be saved
# @return [String] the path to which the file was saved
#
def save_page ( path = nil )
2018-08-20 16:46:31 -07:00
prepare_path ( path , 'html' ) . tap do | p_path |
File . write ( p_path , Capybara :: Helpers . inject_asset_host ( body , host : config . asset_host ) , mode : 'wb' )
2018-01-13 13:06:03 -08:00
end
2011-02-12 21:18:47 +01:00
end
2012-07-24 09:05:50 +02:00
##
#
2014-08-24 14:48:20 +03:00
# Save a snapshot of the page and open it in a browser for inspection.
2012-07-24 09:05:50 +02:00
#
2019-05-27 06:41:27 +09:00
# If invoked without arguments it will save file to {Capybara.configure save_path}
# and file will be given randomly generated filename. If invoked with a relative path
# the path will be relative to {Capybara.configure save_path}.
2012-07-24 09:05:50 +02:00
#
2014-08-24 14:48:20 +03:00
# @param [String] path the path to where it should be saved
#
def save_and_open_page ( path = nil )
2018-08-20 16:46:31 -07:00
save_page ( path ) . tap { | s_path | open_file ( s_path ) }
2010-07-10 01:42:59 +02:00
end
2012-07-10 13:50:15 +09:00
##
#
2014-08-24 14:48:20 +03:00
# Save a screenshot of page.
2012-07-10 13:50:15 +09:00
#
2019-05-27 06:41:27 +09:00
# If invoked without arguments it will save file to {Capybara.configure save_path}
# and file will be given randomly generated filename. If invoked with a relative path
# the path will be relative to {Capybara.configure save_path}.
2014-08-24 14:48:20 +03:00
#
# @param [String] path the path to where it should be saved
# @param [Hash] options a customizable set of options
# @return [String] the path to which the file was saved
2016-08-17 16:14:39 -07:00
def save_screenshot ( path = nil , ** options )
2019-11-28 14:41:13 -08:00
prepare_path ( path , 'png' ) . tap { | p_path | driver . save_screenshot ( p_path , ** options ) }
2014-03-21 19:09:15 -07:00
end
##
#
2014-08-24 14:48:20 +03:00
# Save a screenshot of the page and open it for inspection.
2014-03-21 19:09:15 -07:00
#
2019-05-27 06:41:27 +09:00
# If invoked without arguments it will save file to {Capybara.configure save_path}
# and file will be given randomly generated filename. If invoked with a relative path
# the path will be relative to {Capybara.configure save_path}.
2014-03-21 19:09:15 -07:00
#
2014-08-24 14:48:20 +03:00
# @param [String] path the path to where it should be saved
# @param [Hash] options a customizable set of options
#
2016-08-17 16:14:39 -07:00
def save_and_open_screenshot ( path = nil , ** options )
2019-11-28 14:41:13 -08:00
save_screenshot ( path , ** options ) . tap { | s_path | open_file ( s_path ) } # rubocop:disable Lint/Debugger
2012-07-10 13:50:15 +09:00
end
2010-07-11 14:00:00 +02:00
def document
2011-07-13 15:39:17 +02:00
@document || = Capybara :: Node :: Document . new ( self , driver )
2010-07-11 14:00:00 +02:00
end
2011-03-25 14:22:47 +01:00
NODE_METHODS . each do | method |
2012-09-09 23:36:45 +02:00
define_method method do | * args , & block |
@touched = true
2013-02-17 16:19:26 +01:00
current_scope . send ( method , * args , & block )
2012-09-09 23:36:45 +02:00
end
2010-07-11 14:00:00 +02:00
end
2014-02-16 20:13:58 +03:00
DOCUMENT_METHODS . each do | method |
define_method method do | * args , & block |
document . send ( method , * args , & block )
2013-02-16 09:34:30 +01:00
end
end
2014-02-16 20:13:58 +03:00
def inspect
%( # <Capybara::Session> )
2013-02-16 09:34:30 +01:00
end
2013-02-17 16:19:26 +01:00
def current_scope
2016-12-08 16:57:45 -08:00
scope = scopes . last
2018-01-13 13:06:03 -08:00
[ nil , :frame ] . include? ( scope ) ? document : scope
2009-11-26 23:47:58 +01:00
end
2016-12-15 09:04:01 -08:00
##
#
2019-05-27 06:41:27 +09:00
# Yield a block using a specific maximum wait time.
2016-12-15 09:04:01 -08:00
#
def using_wait_time ( seconds )
if Capybara . threadsafe
begin
previous_wait_time = config . default_max_wait_time
config . default_max_wait_time = seconds
yield
ensure
config . default_max_wait_time = previous_wait_time
end
else
Capybara . using_wait_time ( seconds ) { yield }
end
end
##
#
2019-05-27 06:41:27 +09:00
# Accepts a block to set the configuration options if {Capybara.configure threadsafe} is `true`. Note that some options only have an effect
# if set at initialization time, so look at the configuration block that can be passed to the initializer too.
2016-12-15 09:04:01 -08:00
#
def configure
2018-07-10 14:18:39 -07:00
raise 'Session configuration is only supported when Capybara.threadsafe == true' unless Capybara . threadsafe
2018-09-24 09:43:46 -07:00
2016-12-15 09:04:01 -08:00
yield config
end
def self . instance_created?
@@instance_created
end
def config
@config || = if Capybara . threadsafe
Capybara . session_options . dup
else
Capybara :: ReadOnlySessionConfig . new ( Capybara . session_options )
end
end
2018-01-09 14:05:50 -08:00
2019-07-07 15:45:20 -07:00
def server_url
@server & . base_url
end
2013-02-17 16:19:26 +01:00
private
2016-12-15 09:04:01 -08:00
2019-05-08 16:03:57 -07:00
@@instance_created = false # rubocop:disable Style/ClassVars
2016-12-15 09:04:01 -08:00
2018-05-15 21:43:45 -07:00
def driver_args ( args )
args . map { | arg | arg . is_a? ( Capybara :: Node :: Element ) ? arg . base : arg }
end
2016-11-18 10:56:47 -08:00
def accept_modal ( type , text_or_options , options , & blk )
2019-11-28 14:41:13 -08:00
driver . accept_modal ( type , ** modal_options ( text_or_options , ** options ) , & blk )
2016-11-18 10:56:47 -08:00
end
def dismiss_modal ( type , text_or_options , options , & blk )
2019-11-28 14:41:13 -08:00
driver . dismiss_modal ( type , ** modal_options ( text_or_options , ** options ) , & blk )
2016-11-21 16:28:45 -08:00
end
2018-01-09 14:05:50 -08:00
def modal_options ( text = nil , ** options )
2017-08-08 05:33:20 -07:00
options [ :text ] || = text unless text . nil?
2016-12-15 09:04:01 -08:00
options [ :wait ] || = config . default_max_wait_time
2016-11-21 16:28:45 -08:00
options
2016-11-18 10:56:47 -08:00
end
2014-08-24 14:48:20 +03:00
def open_file ( path )
2018-07-10 14:18:39 -07:00
require 'launchy'
2018-01-09 14:05:50 -08:00
Launchy . open ( path )
rescue LoadError
2018-01-13 13:06:03 -08:00
warn " File saved to #{ path } . \n Please install the launchy gem to open the file automatically. "
2014-03-21 19:09:15 -07:00
end
2014-08-24 14:48:20 +03:00
def prepare_path ( path , extension )
2018-11-02 11:38:58 -07:00
File . expand_path ( path || default_fn ( extension ) , config . save_path ) . tap do | p_path |
FileUtils . mkdir_p ( File . dirname ( p_path ) )
end
2014-08-24 14:48:20 +03:00
end
2016-04-05 09:33:19 -07:00
def default_fn ( extension )
2018-07-10 14:18:39 -07:00
timestamp = Time . new . strftime ( '%Y%m%d%H%M%S' )
2016-11-03 22:14:55 +09:00
" capybara- #{ timestamp } #{ rand ( 10 ** 10 ) } . #{ extension } "
2014-03-21 19:09:15 -07:00
end
2009-11-26 23:47:58 +01:00
def scopes
2014-04-09 11:22:17 +03:00
@scopes || = [ nil ]
2009-11-26 23:47:58 +01:00
end
2017-01-28 14:34:44 -08:00
def element_script_result ( arg )
case arg
when Array
2018-08-20 16:46:31 -07:00
arg . map { | subarg | element_script_result ( subarg ) }
2017-01-28 14:34:44 -08:00
when Hash
2019-03-25 09:37:25 -07:00
arg . transform_values! { | value | element_script_result ( value ) }
2017-01-28 14:34:44 -08:00
when Capybara :: Driver :: Node
Capybara :: Node :: Element . new ( self , arg , nil , nil )
else
arg
end
end
2017-02-13 09:59:51 -08:00
2018-08-24 06:44:21 -07:00
def adjust_server_port ( uri )
uri . port || = @server . port if @server && config . always_include_port
end
2017-02-13 09:59:51 -08:00
def _find_frame ( * args )
2018-07-06 08:56:40 -07:00
return find ( :frame ) if args . length . zero?
2016-08-17 14:07:44 -07:00
case args [ 0 ]
when Capybara :: Node :: Element
args [ 0 ]
when String , Hash
find ( :frame , * args )
when Symbol
find ( * args )
when Integer
idx = args [ 0 ]
2018-01-09 14:05:50 -08:00
all ( :frame , minimum : idx + 1 ) [ idx ]
2016-08-17 14:07:44 -07:00
else
raise TypeError
2017-02-13 09:59:51 -08:00
end
end
2017-06-22 17:21:24 -07:00
2018-08-24 06:44:21 -07:00
def _switch_to_window ( window = nil , ** options , & window_locator )
2018-07-10 14:18:39 -07:00
raise Capybara :: ScopeError , 'Window cannot be switched inside a `within_frame` block' if scopes . include? ( :frame )
2018-08-24 06:44:21 -07:00
raise Capybara :: ScopeError , 'Window cannot be switched inside a `within` block' unless scopes . last . nil?
2017-06-22 17:21:24 -07:00
if window
driver . switch_to_window ( window . handle )
window
else
2018-08-24 06:44:21 -07:00
synchronize_windows ( options ) do
2017-06-22 17:21:24 -07:00
original_window_handle = driver . current_window_handle
begin
2018-08-24 06:44:21 -07:00
_switch_to_window_by_locator ( & window_locator )
rescue StandardError
2017-06-22 17:21:24 -07:00
driver . switch_to_window ( original_window_handle )
2018-08-24 06:44:21 -07:00
raise
2017-06-22 17:21:24 -07:00
end
end
end
end
2018-08-24 06:44:21 -07:00
def _switch_to_window_by_locator
driver . window_handles . each do | handle |
driver . switch_to_window handle
return Window . new ( self , handle ) if yield
end
raise Capybara :: WindowError , 'Could not find a window matching block/lambda'
end
def synchronize_windows ( options , & block )
wait_time = Capybara :: Queries :: BaseQuery . wait ( options , config . default_max_wait_time )
document . synchronize ( wait_time , errors : [ Capybara :: WindowError ] , & block )
end
2009-11-14 15:11:29 +01:00
end
2009-11-07 15:36:58 +01:00
end