Skip to content

Commit

Permalink
Split selector definition and configured selector
Browse files Browse the repository at this point in the history
Allow selectors to specify multiple expression types
  • Loading branch information
twalpole committed Mar 6, 2019
1 parent 5a5782f commit cadad8b
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 298 deletions.
39 changes: 29 additions & 10 deletions lib/capybara/queries/selector_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def initialize(*args,
session_options:,
enable_aria_label: session_options.enable_aria_label,
test_id: session_options.test_id,
selector_format: nil,
**options,
&filter_block)
@resolved_node = nil
Expand All @@ -19,14 +20,18 @@ def initialize(*args,
super(@options)
self.session_options = session_options

@selector = find_selector(args[0].is_a?(Symbol) ? args.shift : args[0])
@selector = Selector.new(
find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
selector_config: { enable_aria_label: enable_aria_label, test_id: test_id },
selector_format: selector_format
)

@locator = args.shift
@filter_block = filter_block

raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?

selector_config = { enable_aria_label: enable_aria_label, test_id: test_id }
@expression = selector.call(@locator, @options.merge(selector_config: selector_config))
@expression = selector.call(@locator, @options)

warn_exact_usage

Expand Down Expand Up @@ -129,7 +134,9 @@ def resolve_for(node, exact = nil)

# @api private
def supports_exact?
@expression.respond_to? :to_xpath
return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?

@selector.supports_exact?
end

def failure_message
Expand All @@ -142,6 +149,10 @@ def negative_failure_message

private

def selector_format
@selector.selector_format
end

def text_fragments
text = (options[:text] || options[:exact_text])
text.is_a?(String) ? text.split : []
Expand Down Expand Up @@ -182,23 +193,23 @@ def find_selector(locator)
def find_nodes_by_selector_format(node, exact)
hints = {}
hints[:uses_visibility] = true unless visible == :all
hints[:texts] = text_fragments unless selector.format == :xpath
hints[:texts] = text_fragments unless selector_format == :xpath
hints[:styles] = options[:style] if use_default_style_filter?

if selector.format == :css
if selector_format == :css
if node.method(:find_css).arity != 1
node.find_css(css, **hints)
else
node.find_css(css)
end
elsif selector.format == :xpath
elsif selector_format == :xpath
if node.method(:find_xpath).arity != 1
node.find_xpath(xpath(exact), **hints)
else
node.find_xpath(xpath(exact))
end
else
raise ArgumentError, "Unknown format: #{selector.format}"
raise ArgumentError, "Unknown format: #{selector_format}"
end
end

Expand All @@ -220,6 +231,8 @@ def matches_node_filters?(node, errors)
unapplied_options = options.keys - valid_keys
@selector.with_filter_errors(errors) do
node_filters.all? do |filter_name, filter|
next true unless apply_filter?(filter)

if filter.matcher?
unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
unapplied_options.delete(option_name)
Expand Down Expand Up @@ -310,6 +323,8 @@ def use_default_style_filter?
def apply_expression_filters(expression)
unapplied_options = options.keys - valid_keys
expression_filters.inject(expression) do |expr, (name, ef)|
next expr unless apply_filter?(ef)

if ef.matcher?
unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
unapplied_options.delete(option_name)
Expand Down Expand Up @@ -348,10 +363,14 @@ def simple_root?(node)
node.is_a?(::Capybara::Node::Simple) && node.path == '/'
end

def apply_filter?(filter)
filter.format.nil? || (filter.format == selector_format)
end

def matches_locator_filter?(node)
return true unless @selector.locator_filter
return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)

@selector.locator_filter.matches?(node, @locator, @selector)
@selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
end

def matches_system_filters?(node)
Expand Down
87 changes: 78 additions & 9 deletions lib/capybara/selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,96 @@
locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
end

Capybara.add_selector(:field, locator_type: [String, Symbol]) do
Capybara.add_selector(:field, locator_type: [String, Symbol], supports_exact: true) do
visible { |options| :hidden if options[:type].to_s == 'hidden' }

xpath do |locator, **options|
invalid_types = %w[submit image]
invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
locate_field(xpath, locator, options)
end

css do |_locator, **options|
invalid_types = %w[submit image]
invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
invalid_attributes = invalid_types.map { |type| ":not([type=#{type}])" }.join
"input#{invalid_attributes}, textarea, select"
end

locator_filter(skip_if: nil, format: :css) do |node, locator, exact:, **_|
optional_checks = +''
optional_checks << "(field.getAttribute('aria-label') == locator)||" if enable_aria_label
optional_checks << "(field.getAttribute('#{test_id}') == locator)||" if test_id

match_js = <<~JS
(function(field, locator){
return (
(field.id == locator) ||
(field.name == locator) ||
(field.placeholder == locator)||
#{optional_checks}
Array.from(field.labels || []).some(function(label){
return label.innerText#{exact ? '== locator' : '.includes(locator)'};
})
);
})(this, arguments[0])
JS
node.evaluate_script(match_js, locator)
end

expression_filter(:type) do |expr, type|
type = type.to_s
if %w[textarea select].include?(type)
expr.self(type.to_sym)
case default_format
when :css
if %w[textarea select].include?(type)
::Capybara::Selector::CSS.split(expr).select do |css_fragment|
css_fragment.start_with? type
end.join(',')
else
::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(type: type)
end
when :xpath
if %w[textarea select].include?(type)
expr.self(type.to_sym)
else
expr[XPath.attr(:type) == type]
end
else
expr[XPath.attr(:type) == type]
raise ArgumentError, "Unknown format type: #{default_format}"
end
end

filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder

node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
expression_filter(:name) do |expr, val|
if default_format == :css
::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(name: val)
else
expr[XPath.attr(:name) == val]
end
end
expression_filter(:placeholder) do |expr, val|
if default_format == :css
::Capybara::Selector::CSSBuilder.new(expr).add_attribute_conditions(placeholder: val)
else
expr[XPath.attr(:placeholder) == val]
end
end
expression_filter(:readonly, :boolean, format: :css) do |expr, val|
::Capybara::Selector::CSS.split(expr).map do |css_fragment|
if val
"#{css_fragment}:read-only"
else
"#{css_fragment}:read-write"
end
end.join(',')
end

node_filter(:readonly, :boolean, format: :xpath) do |node, value|
!(value ^ node.readonly?)
end

node_filter(:with) do |node, with|
val = node.value
(with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
Expand Down Expand Up @@ -169,8 +238,8 @@
Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
label 'link or button'
xpath do |locator, **options|
self.class.all.values_at(:link, :button).map do |selector|
instance_exec(locator, options, &selector.xpath)
%i[link button].map do |selector|
expression_for(selector, locator, format: :xpath, **options)
end.reduce(:union)
end

Expand Down Expand Up @@ -284,7 +353,7 @@

expression_filter(:with_options) do |expr, options|
options.inject(expr) do |xpath, option|
xpath[self.class.all[:option].call(option)]
xpath[expression_for(:option, option, format: :xpath)]
end
end

Expand Down Expand Up @@ -337,7 +406,7 @@

expression_filter(:with_options) do |expr, options|
options.inject(expr) do |xpath, option|
xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[self.class.all[:datalist_option].call(option)].attr(:id)]
xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option, format: :xpath)].attr(:id)]
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/capybara/selector/css.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'capybara/selector/selector'

module Capybara
class Selector
class CSS
Expand Down
Loading

0 comments on commit cadad8b

Please sign in to comment.