diff --git a/lib/capybara/selector/definition/datalist_input.rb b/lib/capybara/selector/definition/datalist_input.rb index 5cbd43e03..f51f2b786 100644 --- a/lib/capybara/selector/definition/datalist_input.rb +++ b/lib/capybara/selector/definition/datalist_input.rb @@ -19,7 +19,7 @@ expression_filter(:with_options) do |expr, options| options.inject(expr) do |xpath, option| - xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id)] + xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option, format: :xpath)].attr(:id)] end end diff --git a/lib/capybara/selector/definition/field.rb b/lib/capybara/selector/definition/field.rb index 6d2782db1..75c336cbd 100644 --- a/lib/capybara/selector/definition/field.rb +++ b/lib/capybara/selector/definition/field.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -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| @@ -10,18 +10,85 @@ 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 diff --git a/lib/capybara/selector/definition/link_or_button.rb b/lib/capybara/selector/definition/link_or_button.rb index 27b8bd2f0..9cb471984 100644 --- a/lib/capybara/selector/definition/link_or_button.rb +++ b/lib/capybara/selector/definition/link_or_button.rb @@ -4,7 +4,7 @@ label 'link or button' xpath do |locator, **options| %i[link button].map do |selector| - expression_for(selector, locator, **options) + expression_for(selector, locator, format: :xpath, **options) end.reduce(:union) end diff --git a/lib/capybara/selector/definition/select.rb b/lib/capybara/selector/definition/select.rb index 5e11d7416..a160579eb 100644 --- a/lib/capybara/selector/definition/select.rb +++ b/lib/capybara/selector/definition/select.rb @@ -23,7 +23,7 @@ expression_filter(:with_options) do |expr, options| options.inject(expr) do |xpath, option| - xpath[expression_for(:option, option)] + xpath[expression_for(:option, option, format: :xpath)] end end