diff --git a/lib/capybara/node/actions.rb b/lib/capybara/node/actions.rb index 51c19924..9b597434 100644 --- a/lib/capybara/node/actions.rb +++ b/lib/capybara/node/actions.rb @@ -174,8 +174,42 @@ module Capybara # # @return [Capybara::Node::Element] The option element selected def select(value = nil, from: nil, **options) - scope = from ? find(:select, from, options) : self - scope.find(:option, value, options).select_option + scope = if from + synchronize(Capybara::Queries::BaseQuery.wait(options, session_options.default_max_wait_time)) do + begin + find(:select, from, options) + rescue Capybara::ElementNotFound => select_error + raise if %i[selected with_selected multiple].any? { |option| options.key?(option) } + begin + find(:datalist_input, from, options) + rescue Capybara::ElementNotFound => dlinput_error + raise Capybara::ElementNotFound, "#{select_error.message} and #{dlinput_error.message}" + end + end + end + else + self + end + + if scope.respond_to?(:tag_name) && scope.tag_name == "input" + begin + # TODO: this is a more efficient but won't work with non-JS drivers + # datalist_options = session.evaluate_script('Array.prototype.slice.call((arguments[0].list||{}).options || []).filter(function(el){ return !el.disabled }).map(function(el){ return { "value": el.value, "label": el.label} })', scope) + datalist_options = session.evaluate_script(DATALIST_OPTIONS_SCRIPT, scope) + if (option = datalist_options.find { |o| o['value'] == value || o['label'] == value }) + scope.set(option["value"]) + else + raise ::Capybara::ElementNotFound, "Unable to find datalist option \"#{value}\"" + end + rescue ::Capybara::NotSupportedByDriverError + # Implement for drivers that don't support JS + datalist = find(:xpath, XPath.descendant(:datalist)[XPath.attr(:id) == scope[:list]], visible: false) + option = datalist.find(:datalist_option, value, disabled: false) + scope.set(option.value) + end + else + scope.find(:option, value, options).select_option + end end ## @@ -291,6 +325,12 @@ module Capybara delete el.capybara_style_cache; } JS + + DATALIST_OPTIONS_SCRIPT = <<-'JS'.freeze + Array.prototype.slice.call((arguments[0].list||{}).options || []). + filter(function(el){ return !el.disabled }). + map(function(el){ return { "value": el.value, "label": el.label} }) + JS end end end diff --git a/lib/capybara/rack_test/node.rb b/lib/capybara/rack_test/node.rb index 42e93512..d6105f59 100644 --- a/lib/capybara/rack_test/node.rb +++ b/lib/capybara/rack_test/node.rb @@ -90,7 +90,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node return true if string_node.disabled? if %w[option optgroup].include? tag_name - find_xpath("parent::*[self::optgroup or self::select]")[0].disabled? + find_xpath("parent::*[self::optgroup or self::select or self::datalist]")[0].disabled? else !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty? end diff --git a/lib/capybara/selector.rb b/lib/capybara/selector.rb index e17fa768..802f34e9 100644 --- a/lib/capybara/selector.rb +++ b/lib/capybara/selector.rb @@ -407,6 +407,34 @@ Capybara.add_selector(:select) do end end +Capybara.add_selector(:datalist_input) do + label "input box with datalist completion" + + xpath do |locator, **options| + xpath = XPath.descendant(:input)[XPath.attr(:list)] + locate_field(xpath, locator, options) + end + + filter_set(:_field, %i[disabled name placeholder]) + + filter(:options) do |node, options| + actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value) + options.sort == actual.sort + end + + filter(:with_options) do |node, options| + options.all? { |option| node.find("//datalist[@id=#{node[:list]}]", visible: :all).first(:datalist_option, option) } + end + + describe do |options: nil, with_options: nil, **opts| + desc = "".dup + desc << " with options #{options.inspect}" if options + desc << " with at least options #{with_options.inspect}" if with_options + desc << describe_all_expression_filters(opts) + desc + end +end + ## # # Find option elements @@ -433,6 +461,25 @@ Capybara.add_selector(:option) do end end +Capybara.add_selector(:datalist_option) do + label "datalist option" + visible(:all) + + xpath do |locator| + xpath = XPath.descendant(:option) + xpath = xpath[XPath.string.n.is(locator.to_s) | (XPath.attr(:value) == locator.to_s)] unless locator.nil? + xpath + end + + filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) } + + describe do |**options| + desc = "".dup + desc << " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled) + desc + end +end + ## # # Find file input elements diff --git a/lib/capybara/spec/session/select_spec.rb b/lib/capybara/spec/session/select_spec.rb index b48b172e..bda2a27b 100644 --- a/lib/capybara/spec/session/select_spec.rb +++ b/lib/capybara/spec/session/select_spec.rb @@ -84,9 +84,35 @@ Capybara::SpecHelper.spec "#select" do expect(@session.find_field('Title').value).to eq('Miss') end + context "input with datalist" do + it "should select an option" do + @session.select("Audi", from: 'manufacturer') + @session.click_button('awesome') + expect(extract_results(@session)['manufacturer']).to eq('Audi') + end + + it "should not find an input without a datalist" do + expect do + @session.select("Thomas", from: 'form_first_name') + end.to raise_error(/Unable to find visible input box with datalist completion "form_first_name" that is not disabled/) + end + + it "should not select an option that doesn't exist" do + expect do + @session.select("Tata", from: 'manufacturer') + end.to raise_error(/Unable to find datalist option "Tata"/) + end + + it "should not select a disabled option" do + expect do + @session.select("Mercedes", from: 'manufacturer') + end.to raise_error(/Unable to find datalist option "Mercedes"/) + end + end + context "with a locator that doesn't exist" do it "should raise an error" do - msg = "Unable to find visible select box \"does not exist\" that is not disabled" + msg = /Unable to find visible select box "does not exist" that is not disabled/ expect do @session.select('foo', from: 'does not exist') end.to raise_error(Capybara::ElementNotFound, msg) diff --git a/lib/capybara/spec/views/form.erb b/lib/capybara/spec/views/form.erb index d05320b3..4367a682 100644 --- a/lib/capybara/spec/views/form.erb +++ b/lib/capybara/spec/views/form.erb @@ -197,6 +197,15 @@ New line after and before textarea tag

+

+ + + + +

+