From 6ccd301a8b705b8180dda08232b254057eea07e1 Mon Sep 17 00:00:00 2001 From: Federico Franco Date: Sun, 1 Feb 2026 17:32:36 +0900 Subject: [PATCH 1/3] [rb] fix select being able to select options hidden by css rules --- rb/lib/selenium/webdriver/support/select.rb | 19 +++++++++++++++++++ .../selenium/webdriver/select_spec.rb | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/rb/lib/selenium/webdriver/support/select.rb b/rb/lib/selenium/webdriver/support/select.rb index f5ad753f14914..aa2ec5a0d9d24 100644 --- a/rb/lib/selenium/webdriver/support/select.rb +++ b/rb/lib/selenium/webdriver/support/select.rb @@ -217,10 +217,20 @@ def deselect_by_index(index) def select_option(option) raise Error::UnsupportedOperationError, 'You may not select a disabled option' unless option.enabled? + unless css_property_and_visible?(option) + raise Error::ElementNotInteractableError, + 'You may not select an invisible option' + end + option.click unless option.selected? end def deselect_option(option) + unless css_property_and_visible?(option) + raise Error::ElementNotInteractableError, + 'You may not deselect an invisible option' + end + option.click if option.selected? end @@ -266,6 +276,15 @@ def find_by_index(index) def find_by_value(value) @element.find_elements(xpath: ".//option[@value = #{Escaper.escape value}]") end + + def css_property_and_visible?(element) + css_value_candidates = %w[hidden none 0 0.0].to_set + css_property_candidates = %w[visibility display opacity] + + css_property_candidates.none? do |property| + css_value_candidates.include?(element.css_value(property)) + end + end end # Select end # Support end # WebDriver diff --git a/rb/spec/integration/selenium/webdriver/select_spec.rb b/rb/spec/integration/selenium/webdriver/select_spec.rb index dbea49e93584a..d7e78b800f770 100644 --- a/rb/spec/integration/selenium/webdriver/select_spec.rb +++ b/rb/spec/integration/selenium/webdriver/select_spec.rb @@ -27,6 +27,7 @@ module Support let(:multi_select) { described_class.new(driver.find_element(id: 'multi')) } let(:single_disabled) { described_class.new(driver.find_element(name: 'single_disabled')) } let(:multi_disabled) { described_class.new(driver.find_element(name: 'multi_disabled')) } + let(:multi_invisible) { described_class.new(driver.find_element(id: 'invisible_multi_select')) } before { driver.navigate.to url_for('formPage.html') } after { reset_driver! if GlobalTestEnv.rbe? && GlobalTestEnv.browser == :chrome } @@ -120,6 +121,14 @@ module Support it 'errors when not found' do expect { multi_select.select_by(:text, 'invalid') }.to raise_exception(Error::NoSuchElementError) end + + it 'errors when option is invisible', :aggregate_failures do + %w[Apples Pears Oranges Lemons].each do |text| + expect { + multi_invisible.select_by(:text, text) + }.to raise_exception(Error::ElementNotInteractableError) + end + end end context 'when by index' do @@ -298,6 +307,14 @@ module Support it 'errors when not found' do expect { multi_select.deselect_by(:text, 'invalid') }.to raise_exception(Error::NoSuchElementError) end + + it 'errors when option is invisible', :aggregate_failures do + %w[Apples Pears Oranges Lemons].each do |text| + expect { + multi_invisible.deselect_by(:text, text) + }.to raise_exception(Error::ElementNotInteractableError) + end + end end context 'when by index' do From b1e0eaea0eae3a268b3631610285f5682b34b928 Mon Sep 17 00:00:00 2001 From: Federico Franco Date: Mon, 27 Apr 2026 20:24:14 +0900 Subject: [PATCH 2/3] [rb] refactoring, adding tests --- rb/lib/selenium/webdriver/support/select.rb | 31 +++++++------------ .../selenium/webdriver/select_spec.rb | 31 ++++++++++++++----- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/rb/lib/selenium/webdriver/support/select.rb b/rb/lib/selenium/webdriver/support/select.rb index aa2ec5a0d9d24..daf7624f72b7c 100644 --- a/rb/lib/selenium/webdriver/support/select.rb +++ b/rb/lib/selenium/webdriver/support/select.rb @@ -21,6 +21,9 @@ module Selenium module WebDriver module Support class Select + HIDDEN_CSS_VALUES = %w[hidden none 0 0.0].to_set.freeze + VISIBILITY_PROPERTIES = %w[visibility display opacity].freeze + # # @param [Element] element The select element to use # @@ -163,9 +166,14 @@ def deselect_all def select_by_text(text) opts = find_by_text text - return select_options(opts) unless opts.empty? + raise Error::NoSuchElementError, "cannot locate element with text: #{text.inspect}" if opts.empty? - raise Error::NoSuchElementError, "cannot locate element with text: #{text.inspect}" + opts.each do |opt| + raise Error::NoSuchElementError, "invisible option with text: #{text.inspect}" unless option_visible?(opt) + + select_option(opt) + break unless multiple? + end end def select_by_index(index) @@ -217,20 +225,10 @@ def deselect_by_index(index) def select_option(option) raise Error::UnsupportedOperationError, 'You may not select a disabled option' unless option.enabled? - unless css_property_and_visible?(option) - raise Error::ElementNotInteractableError, - 'You may not select an invisible option' - end - option.click unless option.selected? end def deselect_option(option) - unless css_property_and_visible?(option) - raise Error::ElementNotInteractableError, - 'You may not deselect an invisible option' - end - option.click if option.selected? end @@ -277,13 +275,8 @@ def find_by_value(value) @element.find_elements(xpath: ".//option[@value = #{Escaper.escape value}]") end - def css_property_and_visible?(element) - css_value_candidates = %w[hidden none 0 0.0].to_set - css_property_candidates = %w[visibility display opacity] - - css_property_candidates.none? do |property| - css_value_candidates.include?(element.css_value(property)) - end + def option_visible?(element) + VISIBILITY_PROPERTIES.none? { |property| HIDDEN_CSS_VALUES.include?(element.css_value(property)) } end end # Select end # Support diff --git a/rb/spec/integration/selenium/webdriver/select_spec.rb b/rb/spec/integration/selenium/webdriver/select_spec.rb index d7e78b800f770..706fe43573219 100644 --- a/rb/spec/integration/selenium/webdriver/select_spec.rb +++ b/rb/spec/integration/selenium/webdriver/select_spec.rb @@ -126,7 +126,7 @@ module Support %w[Apples Pears Oranges Lemons].each do |text| expect { multi_invisible.select_by(:text, text) - }.to raise_exception(Error::ElementNotInteractableError) + }.to raise_exception(Error::NoSuchElementError) end end end @@ -156,6 +156,11 @@ module Support it 'errors when not found' do expect { multi_select.select_by(:index, 5) }.to raise_exception(Error::NoSuchElementError) end + + it 'selects invisible option' do + multi_invisible.select_by(:index, 1) + expect(multi_invisible.selected_options).to include(driver.find_element(css: 'option[value=pears]')) + end end context 'when by value' do @@ -185,6 +190,11 @@ module Support it 'errors when not found' do expect { multi_select.select_by(:value, 'invalid') }.to raise_exception(Error::NoSuchElementError) end + + it 'selects invisible option' do + multi_invisible.select_by(:value, 'pears') + expect(multi_invisible.selected_options).to include(driver.find_element(css: 'option[value=pears]')) + end end end @@ -308,12 +318,9 @@ module Support expect { multi_select.deselect_by(:text, 'invalid') }.to raise_exception(Error::NoSuchElementError) end - it 'errors when option is invisible', :aggregate_failures do - %w[Apples Pears Oranges Lemons].each do |text| - expect { - multi_invisible.deselect_by(:text, text) - }.to raise_exception(Error::ElementNotInteractableError) - end + it 'deselects invisible option' do + multi_invisible.deselect_by(:text, 'Apples') + expect(multi_invisible.selected_options).not_to include(driver.find_element(css: 'option[value=apples]')) end end @@ -337,6 +344,11 @@ module Support it 'errors when not found' do expect { multi_select.deselect_by(:index, 5) }.to raise_exception(Error::NoSuchElementError) end + + it 'deselects invisible option' do + multi_invisible.deselect_by(:index, 0) + expect(multi_invisible.selected_options).not_to include(driver.find_element(css: 'option[value=apples]')) + end end context 'when by value' do @@ -359,6 +371,11 @@ module Support it 'errors when not found' do expect { multi_select.deselect_by(:value, 'invalid') }.to raise_exception(Error::NoSuchElementError) end + + it 'deselects invisible option' do + multi_invisible.deselect_by(:value, 'apples') + expect(multi_invisible.selected_options).not_to include(driver.find_element(css: 'option[value=apples]')) + end end end From b0a4c10527901850d0ffe6d4f942459366cc3037 Mon Sep 17 00:00:00 2001 From: Federico Franco Date: Tue, 19 May 2026 20:39:51 +0900 Subject: [PATCH 3/3] [rb] match Java by adding enabled/visible check --- rb/lib/selenium/webdriver/support/select.rb | 17 +++++++++++++++++ .../selenium/webdriver/support/select_spec.rb | 8 ++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/rb/lib/selenium/webdriver/support/select.rb b/rb/lib/selenium/webdriver/support/select.rb index daf7624f72b7c..6a362482b26cc 100644 --- a/rb/lib/selenium/webdriver/support/select.rb +++ b/rb/lib/selenium/webdriver/support/select.rb @@ -164,6 +164,9 @@ def deselect_all private def select_by_text(text) + assert_select_enabled + assert_select_visible + opts = find_by_text text raise Error::NoSuchElementError, "cannot locate element with text: #{text.inspect}" if opts.empty? @@ -176,6 +179,20 @@ def select_by_text(text) end end + def assert_select_enabled + return if @element.enabled? + + raise Error::UnsupportedOperationError, + 'You may not select an option in a disabled select' + end + + def assert_select_visible + return if option_visible?(@element) + + raise Error::UnsupportedOperationError, + 'You may not select an option in an invisible select' + end + def select_by_index(index) opts = find_by_index index diff --git a/rb/spec/unit/selenium/webdriver/support/select_spec.rb b/rb/spec/unit/selenium/webdriver/support/select_spec.rb index b3e3242498a71..b623dc9945ea7 100644 --- a/rb/spec/unit/selenium/webdriver/support/select_spec.rb +++ b/rb/spec/unit/selenium/webdriver/support/select_spec.rb @@ -24,14 +24,16 @@ module WebDriver module Support describe Select do let(:select) do - select_element = instance_double(Element, tag_name: 'select') + select_element = instance_double(Element, tag_name: 'select', enabled?: true) allow(select_element).to receive(:dom_attribute).with(:multiple) + allow(select_element).to receive(:css_value).and_return('') select_element end let(:multi_select) do - select_element = instance_double(Element, tag_name: 'select') + select_element = instance_double(Element, tag_name: 'select', enabled?: true) allow(select_element).to receive(:dom_attribute).with(:multiple).and_return 'multiple' + allow(select_element).to receive(:css_value).and_return('') select_element end @@ -94,6 +96,7 @@ module Support it 'allows options to be selected by visible text' do option = instance_double(Element, selected?: false, enabled?: true, click: nil) + allow(option).to receive(:css_value).and_return('') allow(multi_select).to receive(:find_elements).and_return([option]) described_class.new(multi_select).select_by(:text, 'fish') @@ -185,6 +188,7 @@ module Support it 'falls back to slow lookups when "get by visible text fails" and there is a space' do first_option = instance_double(Element, selected?: false, enabled?: true, text: 'foo bar', click: nil) + allow(first_option).to receive(:css_value).and_return('') allow(select).to receive(:find_elements).and_return([], [first_option]) described_class.new(select).select_by(:text, 'foo bar')