Code cleanup

This commit is contained in:
Thomas Walpole 2018-08-17 13:57:12 -07:00
parent 2f8f15e11e
commit 744e9907a7
13 changed files with 172 additions and 128 deletions

View File

@ -214,8 +214,8 @@ module Capybara
# @overload attach_file([locator], paths, **options)
# @macro waiting_behavior
#
# @param [String] locator Which field to attach the file to
# @param [String, Array<String>] paths The path(s) of the file(s) that will be attached, or an array of paths
# @param [String] locator Which field to attach the file to
# @param [String, Array<String>] paths The path(s) of the file(s) that will be attached
#
# @option options [Symbol] match (Capybara.match) The matching strategy to use (:one, :first, :prefer_exact, :smart).
# @option options [Boolean] exact (Capybara.exact) Match the exact label name/contents or accept a partial match.

View File

@ -55,9 +55,7 @@ module Capybara
def _verify_title(title, options)
query = Capybara::Queries::TitleQuery.new(title, options)
synchronize(query.wait) do
yield(query)
end
synchronize(query.wait) { yield(query) }
true
end
end

View File

@ -3,8 +3,7 @@
module Capybara
module Queries
class SelectorQuery < Queries::BaseQuery
attr_accessor :selector, :locator, :options, :expression, :find, :negative
attr_reader :expression, :selector, :locator, :options
VALID_KEYS = COUNT_KEYS + %i[text id class visible exact exact_text match wait filter_set]
VALID_MATCH = %i[first smart prefer_exact one].freeze
@ -25,7 +24,7 @@ module Capybara
raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
@expression = @selector.call(@locator, @options.merge(selector_config: { enable_aria_label: enable_aria_label, test_id: test_id }))
@expression = selector.call(@locator, @options.merge(selector_config: { enable_aria_label: enable_aria_label, test_id: test_id }))
warn_exact_usage
@ -36,22 +35,22 @@ module Capybara
def label; selector.label || selector.name; end
def description(applied = false)
@description = +''
if !applied || @applied_filters
@description << 'visible ' if visible == :visible
@description << 'non-visible ' if visible == :hidden
desc = +''
if !applied || applied_filters
desc << 'visible ' if visible == :visible
desc << 'non-visible ' if visible == :hidden
end
@description << "#{label} #{locator.inspect}"
if !applied || @applied_filters
@description << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
@description << " with exact text #{exact_text}" if exact_text.is_a?(String)
desc << "#{label} #{locator.inspect}"
if !applied || applied_filters
desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
end
@description << " with id #{options[:id]}" if options[:id]
@description << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
@description << selector.description(node_filters: !applied || (@applied_filters == :node), **options)
@description << ' that also matches the custom filter block' if @filter_block && (!applied || (@applied_filters == :node))
@description << " within #{@resolved_node.inspect}" if describe_within?
@description
desc << " with id #{options[:id]}" if options[:id]
desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
desc << selector.description(node_filters: !applied || (applied_filters == :node), **options)
desc << ' that also matches the custom filter block' if @filter_block && (!applied || (applied_filters == :node))
desc << " within #{@resolved_node.inspect}" if describe_within?
desc
end
def applied_description
@ -136,6 +135,10 @@ module Capybara
private
def applied_filters
@applied_filters ||= false
end
def find_selector(locator)
selector = if locator.is_a?(Symbol)
Selector.all.fetch(locator) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
@ -171,7 +174,6 @@ module Capybara
def matches_filter_block?(node)
return true unless @filter_block
if node.respond_to?(:session)
node.session.using_wait_time(0) { @filter_block.call(node) }
else
@ -201,9 +203,9 @@ module Capybara
unless VALID_MATCH.include?(match)
raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
end
unhandled_options = @options.keys - valid_keys
unhandled_options -= @options.keys.select do |option_name|
expression_filters.any? { |_nmae, ef| ef.handles_option? option_name } ||
unhandled_options = @options.keys.reject do |option_name|
valid_keys.include?(option_name) ||
expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
node_filters.any? { |_name, nf| nf.handles_option? option_name }
end
@ -266,22 +268,21 @@ module Capybara
classes[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1))})" }).join
end
def apply_expression_filters(expr)
def apply_expression_filters(expression)
unapplied_options = options.keys - valid_keys
expression_filters.inject(expr) do |memo, (name, ef)|
expression_filters.inject(expression) do |expr, (name, ef)|
if ef.matcher?
unapplied_options.select { |option_name| ef.handles_option?(option_name) }.each do |option_name|
unapplied_options.select { |option_name| ef.handles_option?(option_name) }.inject(expr) do |memo, option_name|
unapplied_options.delete(option_name)
memo = ef.apply_filter(memo, option_name, options[option_name])
ef.apply_filter(memo, option_name, options[option_name])
end
memo
elsif options.key?(name)
unapplied_options.delete(name)
ef.apply_filter(memo, name, options[name])
ef.apply_filter(expr, name, options[name])
elsif ef.default?
ef.apply_filter(memo, name, ef.default)
ef.apply_filter(expr, name, ef.default)
else
memo
expr
end
end
end

View File

@ -62,10 +62,7 @@ module Capybara
if max_idx.nil?
full_results[*args]
else
loop do
break if @result_cache.size > max_idx
@result_cache << @results_enum.next
end
load_up_to(max_idx + 1)
@result_cache[*args]
end
end
@ -77,40 +74,26 @@ module Capybara
def compare_count
# Only check filters for as many elements as necessary to determine result
if @query.options[:count]
count_opt = Integer(@query.options[:count])
loop do
break if @result_cache.size > count_opt
@result_cache << @results_enum.next
end
return @result_cache.size <=> count_opt
if (count = @query.options[:count])
count = Integer(count)
return load_up_to(count + 1) <=> count
end
if @query.options[:minimum]
min_opt = Integer(@query.options[:minimum])
begin
@result_cache << @results_enum.next while @result_cache.size < min_opt
rescue StopIteration
return -1
end
if (min = @query.options[:minimum])
min = Integer(min)
return -1 if load_up_to(min) < min
end
if @query.options[:maximum]
max_opt = Integer(@query.options[:maximum])
loop do
return 1 if @result_cache.size > max_opt
@result_cache << @results_enum.next
end
if (max = @query.options[:maximum])
max = Integer(max)
return 1 if load_up_to(max + 1) > max
end
if @query.options[:between]
min, max = @query.options[:between].minmax
loop do
break if @result_cache.size > max
@result_cache << @results_enum.next
end
return 0 if @query.options[:between].include? @result_cache.size
return @result_cache.size <=> min
if (between = @query.options[:between])
min, max = between.minmax
size = load_up_to(max + 1)
return 0 if between.include? size
return size <=> min
end
0
@ -144,6 +127,14 @@ module Capybara
private
def load_up_to(num)
loop do
break if @result_cache.size >= num
@result_cache << @results_enum.next
end
@result_cache.size
end
def full_results
loop { @result_cache << @results_enum.next }
@result_cache

View File

@ -185,7 +185,6 @@ module Capybara
@match = nil
@label = nil
@failure_message = nil
@description = nil
@format = nil
@expression = nil
@expression_filters = {}

View File

@ -49,7 +49,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
@app = app
@browser = nil
@exit_status = nil
@frame_handles = {}
@frame_handles = Hash.new { |hash, handle| hash[handle] = [] }
@options = DEFAULT_OPTIONS.merge(options)
@node_class = ::Capybara::Selenium::Node
end
@ -170,19 +170,19 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
end
def switch_to_frame(frame)
handles = @frame_handles[current_window_handle]
case frame
when :top
@frame_handles[browser.window_handle] = []
handles.clear
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
handles.pop
browser.switch_to.default_content
@frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
handles.each { |fh| browser.switch_to.frame(fh) }
else
@frame_handles[browser.window_handle] ||= []
@frame_handles[browser.window_handle] << frame.native
handles << frame.native
browser.switch_to.frame(frame.native)
end
end

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true
# Selenium specific implementation of the Capybara::Driver::Node API
class Capybara::Selenium::Node < Capybara::Driver::Node
def visible_text
native.text
@ -82,13 +83,11 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
end
def click(keys = [], **options)
if keys.empty? && !coords?(options)
native.click
else
scroll_if_needed do
action_with_modifiers(keys, options) do |action|
coords?(options) ? action.click : action.click(native)
end
click_options = ClickOptions.new(keys, options)
return native.click if click_options.empty?
scroll_if_needed do
action_with_modifiers(click_options) do |action|
click_options.coords? ? action.click : action.click(native)
end
end
rescue StandardError => err
@ -101,17 +100,19 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
end
def right_click(keys = [], **options)
click_options = ClickOptions.new(keys, options)
scroll_if_needed do
action_with_modifiers(keys, options) do |action|
coords?(options) ? action.context_click : action.context_click(native)
action_with_modifiers(click_options) do |action|
click_options.coords? ? action.context_click : action.context_click(native)
end
end
end
def double_click(keys = [], **options)
click_options = ClickOptions.new(keys, options)
scroll_if_needed do
action_with_modifiers(keys, options) do |action|
coords?(options) ? action.double_click : action.double_click(native)
action_with_modifiers(click_options) do |action|
click_options.coords? ? action.double_click : action.double_click(native)
end
end
end
@ -121,11 +122,11 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
end
def hover
scroll_if_needed { driver.browser.action.move_to(native).perform }
scroll_if_needed { browser_action.move_to(native).perform }
end
def drag_to(element)
scroll_if_needed { driver.browser.action.drag_and_drop(native, element.native).perform }
scroll_if_needed { browser_action.drag_and_drop(native, element.native).perform }
end
def tag_name
@ -192,10 +193,6 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
private
def coords?(options)
options[:x] && options[:y]
end
def boolean_attr(val)
val && (val != 'false')
end
@ -206,22 +203,23 @@ private
end
def set_text(value, clear: nil, **_unused)
if value.to_s.empty? && clear.nil?
value = value.to_s
if value.empty? && clear.nil?
native.clear
elsif clear == :backspace
# Clear field by sending the correct number of backspace keys.
backspaces = [:backspace] * self.value.to_s.length
send_keys(*([:end] + backspaces + [value.to_s]))
send_keys(*([:end] + backspaces + [value]))
elsif clear == :none
send_keys(value.to_s)
send_keys(value)
elsif clear.is_a? Array
send_keys(*clear, value.to_s)
send_keys(*clear, value)
else
# Clear field by JavaScript assignment of the value property.
# Script can change a readonly element which user input cannot, so
# don't execute if readonly.
driver.execute_script "arguments[0].value = ''", self
send_keys(value.to_s)
send_keys(value)
end
end
@ -248,21 +246,24 @@ private
end
def set_date(value) # rubocop:disable Naming/AccessorMethodName
return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_date)
value = SettableValue.new(value)
return set_text(value) unless value.dateable?
# TODO: this would be better if locale can be detected and correct keystrokes sent
update_value_js(value.to_date.strftime('%Y-%m-%d'))
update_value_js(value.to_date_str)
end
def set_time(value) # rubocop:disable Naming/AccessorMethodName
return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_time)
value = SettableValue.new(value)
return set_text(value) unless value.timeable?
# TODO: this would be better if locale can be detected and correct keystrokes sent
update_value_js(value.to_time.strftime('%H:%M'))
update_value_js(value.to_time_str)
end
def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_time)
value = SettableValue.new(value)
return set_text(value) unless value.timeable?
# TODO: this would be better if locale can be detected and correct keystrokes sent
update_value_js(value.to_time.strftime('%Y-%m-%dT%H:%M'))
update_value_js(value.to_datetime_str)
end
def update_value_js(value)
@ -301,34 +302,33 @@ private
# if we use the faster direct send_keys. For now just send_keys to the element
# we've already focused.
# native.send_keys(value.to_s)
driver.browser.action.send_keys(value.to_s).perform
browser_action.send_keys(value.to_s).perform
end
def action_with_modifiers(keys, x: nil, y: nil)
actions = driver.browser.action
actions.move_to(native, x, y)
modifiers_down(actions, keys)
def action_with_modifiers(click_options)
actions = browser_action.move_to(native, *click_options.coords)
modifiers_down(actions, click_options.keys)
yield actions
modifiers_up(actions, keys)
modifiers_up(actions, click_options.keys)
actions.perform
ensure
act = driver.browser.action
act = browser_action
act.release_actions if act.respond_to?(:release_actions)
end
def modifiers_down(actions, keys)
keys.each do |key|
key = case key
when :ctrl then :control
when :command, :cmd then :meta
else
key
end
actions.key_down(key)
end
each_key(keys) { |key| actions.key_down(key) }
end
def modifiers_up(actions, keys)
each_key(keys) { |key| actions.key_up(key) }
end
def browser_action
driver.browser.action
end
def each_key(keys)
keys.each do |key|
key = case key
when :ctrl then :control
@ -336,7 +336,60 @@ private
else
key
end
actions.key_up(key)
yield key
end
end
# SettableValue encapsulates time/date field formatting
class SettableValue
attr_reader :value
def initialize(value)
@value = value
end
def dateable?
!value.is_a?(String) && value.respond_to?(:to_date)
end
def to_date_str
value.to_date.strftime('%Y-%m-%d')
end
def timeable?
!value.is_a?(String) && value.respond_to?(:to_time)
end
def to_time_str
value.to_time.strftime('%H:%M')
end
def to_datetime_str
value.to_time.strftime('%Y-%m-%dT%H:%M')
end
end
private_constant :SettableValue
# ClickOptions encapsulates click option logic
class ClickOptions
attr_reader :keys, :options
def initialize(keys, options)
@keys = keys
@options = options
end
def coords?
options[:x] && options[:y]
end
def coords
[options[:x], options[:y]]
end
def empty?
keys.empty? && !coords?
end
end
private_constant :ClickOptions
end

View File

@ -13,7 +13,7 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
def drag_to(element)
return super unless self[:draggable] == 'true'
scroll_if_needed { driver.browser.action.click_and_hold(native).perform }
scroll_if_needed { browser_action.click_and_hold(native).perform }
driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
end

View File

@ -47,17 +47,15 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
native.click
actions = driver.browser.action
args.each do |keys|
args.each_with_object(browser_action) do |keys, actions|
_send_keys(keys, actions)
end
actions.perform
end.perform
end
def drag_to(element)
return super unless (browser_version >= 62.0) && (self[:draggable] == 'true')
scroll_if_needed { driver.browser.action.click_and_hold(native).perform }
scroll_if_needed { browser_action.click_and_hold(native).perform }
driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
end

View File

@ -31,7 +31,7 @@ module Capybara
end
def reset_error!
middleware.error = nil
middleware.clear_error
end
def error

View File

@ -20,7 +20,7 @@ module Capybara
end
end
attr_accessor :error
attr_reader :error
def initialize(app, server_errors, extra_middleware = [])
@app = app
@ -35,6 +35,10 @@ module Capybara
@counter.value.positive?
end
def clear_error
@error = nil
end
def call(env)
if env['PATH_INFO'] == '/__identify__'
[200, {}, [@app.object_id.to_s]]

View File

@ -137,7 +137,7 @@ module Capybara
# Raise errors encountered in the server
#
def raise_server_error!
return if @server.nil? || !@server.error
return unless @server&.error
# Force an explanation for the error being raised as the exception cause
begin
if config.raise_server_errors

View File

@ -323,7 +323,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
end
end
describe 'Capybara#disable_animation', :focus_ do
describe 'Capybara#disable_animation' do
context 'when set to `true`' do
before(:context) do # rubocop:disable RSpec/BeforeAfterAll
# NOTE: Although Capybara.SpecHelper.reset! sets Capybara.disable_animation to false,