Allow "select" of datalist values from input element

This commit is contained in:
Thomas Walpole 2018-04-26 03:04:31 -07:00
parent 09924678e8
commit da5e5aae98
5 changed files with 126 additions and 4 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -197,6 +197,15 @@ New line after and before textarea tag
</label>
</p>
<p>
<input type="text" name="form[manufacturer]" list="manufacturers" id="manufacturer"/>
<datalist id="manufacturers">
<option value="Jaguar">J</option>
<option value="Audi">
<option value="Mercedes" disabled>
</datalist>
</p>
<p>
<input type="checkbox" name="form[valueless_checkbox]" id="valueless_checkbox" checked="checked"/>
<label for="valueless_checkbox">Valueless Checkbox</label>