diff --git a/History.md b/History.md index 74da8c21..1c781766 100644 --- a/History.md +++ b/History.md @@ -5,6 +5,7 @@ Release date: unreleased * Element#visible?/checked?/disabled?/selected? Now return boolean as expected when using the rack_test driver [Thomas Walpole] * The rack_test driver now considers \ elements as non-visible [Thomas Walpole] +* A nil locator passed to the built-in html type selectors now behaves consistently, and finds all elements of the correct type [Thomas Walpole] ### Added * :multiple filter added to relevant selectors [Thomas Walpole] diff --git a/capybara.gemspec b/capybara.gemspec index 57561ba0..0fa2f467 100644 --- a/capybara.gemspec +++ b/capybara.gemspec @@ -37,6 +37,7 @@ Gem::Specification.new do |s| s.add_development_dependency("cucumber", [">= 0.10.5"]) s.add_development_dependency("rake", ["< 11.0"]) s.add_development_dependency("pry") + s.add_development_dependency("byebug") if RUBY_ENGINE == 'rbx' then s.add_development_dependency("racc") diff --git a/lib/capybara/selector.rb b/lib/capybara/selector.rb index 0a00b7a0..706c2bb0 100644 --- a/lib/capybara/selector.rb +++ b/lib/capybara/selector.rb @@ -119,6 +119,17 @@ module Capybara def describe &block @description = block end + + private + + def locate_field(xpath, locator) + locate_field = xpath[XPath.attr(:id).equals(locator) | + XPath.attr(:name).equals(locator) | + XPath.attr(:placeholder).equals(locator) | + XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))] + locate_field += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath) + locate_field + end end end @@ -135,7 +146,11 @@ Capybara.add_selector(:id) do end Capybara.add_selector(:field) do - xpath { |locator| XPath::HTML.field(locator) } + xpath do |locator| + xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')] + xpath = locate_field(xpath, locator.to_s) unless locator.nil? + xpath + end filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) } filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) } filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } @@ -164,18 +179,26 @@ Capybara.add_selector(:field) do end Capybara.add_selector(:fieldset) do - xpath { |locator| XPath::HTML.fieldset(locator) } -end - -Capybara.add_selector(:link_or_button) do - label "link or button" - xpath { |locator| XPath::HTML.link_or_button(locator) } - filter(:disabled, default: false, boolean: true) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) } - describe { |options| " that is disabled" if options[:disabled] } + xpath do |locator| + xpath = XPath.descendant(:fieldset) + xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]] unless locator.nil? + xpath + end end Capybara.add_selector(:link) do - xpath { |locator| XPath::HTML.link(locator) } + xpath do |locator| + xpath = XPath.descendant(:a)[XPath.attr(:href)] + unless locator.nil? + locator = locator.to_s + xpath = xpath[XPath.attr(:id).equals(locator) | + XPath.string.n.is(locator) | + XPath.attr(:title).is(locator) | + XPath.descendant(:img)[XPath.attr(:alt).is(locator)]] + end + xpath + end + filter(:href) do |node, href| if href.is_a? Regexp node[:href].match href @@ -183,20 +206,53 @@ Capybara.add_selector(:link) do node.first(:xpath, XPath.axis(:self)[XPath.attr(:href).equals(href.to_s)], minimum: 0) end end + describe { |options| " with href #{options[:href].inspect}" if options[:href] } end Capybara.add_selector(:button) do - xpath { |locator| XPath::HTML.button(locator) } + xpath do |locator| + input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')] + btn_xpath = XPath.descendant(:button) + image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).equals('image')] + + unless locator.nil? + locator = locator.to_s + input_btn_xpath = input_btn_xpath[XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)] + btn_xpath = btn_xpath[XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.string.n.is(locator) | XPath.attr(:title).is(locator)] + image_btn_xpath = image_btn_xpath[XPath.attr(:alt).is(locator)] + end + + input_btn_xpath + btn_xpath + image_btn_xpath + end + filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } + describe { |options| " that is disabled" if options[:disabled] == true } end +Capybara.add_selector(:link_or_button) do + label "link or button" + xpath do |locator| + self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator)}.reduce(:+) + end + + filter(:disabled, default: false, boolean: true) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) } + + describe { |options| " that is disabled" if options[:disabled] } +end + Capybara.add_selector(:fillable_field) do label "field" - xpath { |locator| XPath::HTML.fillable_field(locator) } + xpath do |locator| + xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')] + xpath = locate_field(xpath, locator.to_s) unless locator.nil? + xpath + end + filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) } + describe do |options| desc = String.new desc << " that is disabled" if options[:disabled] == true @@ -208,11 +264,17 @@ end Capybara.add_selector(:radio_button) do label "radio button" - xpath { |locator| XPath::HTML.radio_button(locator) } + xpath do |locator| + xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')] + xpath = locate_field(xpath, locator.to_s) unless locator.nil? + xpath + end + filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) } filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) } filter(:option) { |node, value| node.value == value.to_s } filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } + describe do |options| desc, states = String.new, [] desc << " with value #{options[:option].inspect}" if options[:option] @@ -225,11 +287,17 @@ Capybara.add_selector(:radio_button) do end Capybara.add_selector(:checkbox) do - xpath { |locator| XPath::HTML.checkbox(locator) } + xpath do |locator| + xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')] + xpath = locate_field(xpath, locator.to_s) unless locator.nil? + xpath + end + filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) } filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) } filter(:option) { |node, value| node.value == value.to_s } filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } + describe do |options| desc, states = String.new, [] desc << " with value #{options[:option].inspect}" if options[:option] @@ -243,7 +311,12 @@ end Capybara.add_selector(:select) do label "select box" - xpath { |locator| XPath::HTML.select(locator) } + xpath do |locator| + xpath = XPath.descendant(:select) + xpath = locate_field(xpath, locator.to_s) unless locator.nil? + xpath + end + filter(:options) do |node, options| if node.visible? actual = node.all(:xpath, './/option').map { |option| option.text } @@ -265,6 +338,7 @@ Capybara.add_selector(:select) do end filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) } + describe do |options| desc = String.new desc << " with options #{options[:options].inspect}" if options[:options] @@ -278,14 +352,24 @@ Capybara.add_selector(:select) do end Capybara.add_selector(:option) do - xpath { |locator| XPath::HTML.option(locator) } + xpath do |locator| + xpath = XPath.descendant(:option) + xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil? + xpath + end end Capybara.add_selector(:file_field) do label "file field" - xpath { |locator| XPath::HTML.file_field(locator) } + xpath do |locator| + xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')] + xpath = locate_field(xpath, locator.to_s) unless locator.nil? + xpath + end + filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) } filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) } + describe do |options| desc = String.new desc << " that is disabled" if options[:disabled] == true @@ -296,5 +380,9 @@ Capybara.add_selector(:file_field) do end Capybara.add_selector(:table) do - xpath { |locator| XPath::HTML.table(locator) } + xpath do |locator| + xpath = XPath.descendant(:table) + xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.descendant(:caption).is(locator.to_s)] unless locator.nil? + xpath + end end diff --git a/spec/selector_spec.rb b/spec/selector_spec.rb index 49cb4c6a..369761a8 100644 --- a/spec/selector_spec.rb +++ b/spec/selector_spec.rb @@ -18,6 +18,18 @@ RSpec.describe Capybara do

Some Content

+ + + + + link +
+ + + +
STRING @@ -50,5 +62,28 @@ RSpec.describe Capybara do expect(string).to have_selector(:custom_selector, 'b', not_empty: :all, count: 2) end end + + describe "builtin selectors with nil locators" do + it "devolves to just finding element types" do + selectors = { + field: ".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]", + fieldset: ".//fieldset", + link: ".//a[./@href]", + link_or_button: ".//a[./@href] | .//input[./@type = 'submit' or ./@type = 'reset' or ./@type = 'image' or ./@type = 'button'] | .//button" , + fillable_field: ".//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')]", + radio_button: ".//input[./@type = 'radio']", + checkbox: ".//input[./@type = 'checkbox']", + select: ".//select", + option: ".//option", + file_field: ".//input[./@type = 'file']", + table: ".//table" + } + selectors.each do |selector, xpath| + results = string.all(selector,nil).to_a.map &:native + expect(results.size).to be > 0 + expect(results).to eq string.all(:xpath, xpath).to_a.map(&:native) + end + end + end end end