From bb6641f32d6a68319008153231a1396b4bf48dec Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Sat, 6 Dec 2025 20:14:35 +0200 Subject: [PATCH 1/4] fix flaky Ruby test `element_spec.rb` Wait until the page gets loaded and the element ges rendered. Example of failure: ```ruby Selenium::WebDriver::Element raises if different element receives click Failure/Error: expect { driver.find_element(id: 'contents').click }.to raise_error(Error::ElementClickInterceptedError) expected Selenium::WebDriver::Error::ElementClickInterceptedError, got # with backtrace: # ./rb/lib/selenium/webdriver/remote/response.rb:63:in `add_cause' # ./rb/lib/selenium/webdriver/remote/response.rb:41:in `error' # ./rb/lib/selenium/webdriver/remote/response.rb:52:in `assert_ok' # ./rb/lib/selenium/webdriver/remote/response.rb:34:in `initialize' # ./rb/lib/selenium/webdriver/remote/http/common.rb:103:in `new' # ./rb/lib/selenium/webdriver/remote/http/common.rb:103:in `create_response' # ./rb/lib/selenium/webdriver/remote/http/default.rb:103:in `request' # ./rb/lib/selenium/webdriver/remote/http/common.rb:68:in `call' # ./rb/lib/selenium/webdriver/remote/bridge.rb:625:in `execute' # ./rb/lib/selenium/webdriver/remote/bridge.rb:493:in `find_element_by' # ./rb/lib/selenium/webdriver/common/search_context.rb:71:in `find_element' # ./rb/spec/integration/selenium/webdriver/element_spec.rb:34:in `block (3 levels) in ' # ./rb/spec/integration/selenium/webdriver/element_spec.rb:34:in `block (2 levels) in ' # ./rb/spec/integration/selenium/webdriver/element_spec.rb:34:in `block (2 levels) in ' ``` See https://github.com/SeleniumHQ/selenium/actions/runs/19987626355/job/57324241360?pr=13739 for example. --- .../selenium/webdriver/element_spec.rb | 92 ++++++++++--------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/element_spec.rb b/rb/spec/integration/selenium/webdriver/element_spec.rb index 3884ca724d9d7..0d5995eaa1e7e 100644 --- a/rb/spec/integration/selenium/webdriver/element_spec.rb +++ b/rb/spec/integration/selenium/webdriver/element_spec.rb @@ -24,25 +24,28 @@ module WebDriver describe Element, exclusive: {bidi: false, reason: 'Not yet implemented with BiDi'} do it 'clicks' do driver.navigate.to url_for('formPage.html') - expect { driver.find_element(id: 'imageButton').click }.not_to raise_error + element = wait_for_element(id: 'imageButton') + expect { element.click }.not_to raise_error reset_driver!(time: 1) if %i[safari safari_preview].include? GlobalTestEnv.browser end # Safari returns "click intercepted" error instead of "element click intercepted" it 'raises if different element receives click', except: {browser: %i[safari safari_preview]} do driver.navigate.to url_for('click_tests/overlapping_elements.html') - expect { driver.find_element(id: 'contents').click }.to raise_error(Error::ElementClickInterceptedError) + element = wait_for_element(id: 'contents') + expect { element.click }.to raise_error(Error::ElementClickInterceptedError) end # Safari returns "click intercepted" error instead of "element click intercepted" it 'raises if element is partially covered', except: {browser: %i[safari safari_preview]} do driver.navigate.to url_for('click_tests/overlapping_elements.html') - expect { driver.find_element(id: 'other_contents').click }.to raise_error(Error::ElementClickInterceptedError) + element = wait_for_element(id: 'other_contents') + expect { element.click }.to raise_error(Error::ElementClickInterceptedError) end it 'raises if element stale' do driver.navigate.to url_for('formPage.html') - button = driver.find_element(id: 'imageButton') + button = wait_for_element(id: 'imageButton') driver.navigate.refresh expect { button.click }.to raise_exception(Error::StaleElementReferenceError, @@ -54,7 +57,7 @@ module WebDriver describe '#submit' do it 'valid submit button' do driver.navigate.to url_for('formPage.html') - driver.find_element(id: 'submitButton').submit + wait_for_element(id: 'submitButton').submit sleep 0.5 expect(driver.title).to eq('We Arrive Here') @@ -62,7 +65,7 @@ module WebDriver it 'any input element in form' do driver.navigate.to url_for('formPage.html') - driver.find_element(id: 'checky').submit + wait_for_element(id: 'checky').submit sleep 0.5 expect(driver.title).to eq('We Arrive Here') @@ -70,7 +73,7 @@ module WebDriver it 'any element in form' do driver.navigate.to url_for('formPage.html') - driver.find_element(css: 'form > p').submit + wait_for_element(css: 'form > p').submit sleep 0.5 expect(driver.title).to eq('We Arrive Here') @@ -78,7 +81,7 @@ module WebDriver it 'button with id submit' do driver.navigate.to url_for('formPage.html') - driver.find_element(id: 'submit').submit + wait_for_element(id: 'submit').submit sleep 0.5 expect(driver.title).to eq('We Arrive Here') @@ -86,7 +89,7 @@ module WebDriver it 'button with name submit' do driver.navigate.to url_for('formPage.html') - driver.find_element(name: 'submit').submit + wait_for_element(name: 'submit').submit sleep 0.5 expect(driver.title).to eq('We Arrive Here') @@ -94,7 +97,8 @@ module WebDriver it 'errors with button outside form' do driver.navigate.to url_for('formPage.html') - expect { driver.find_element(name: 'SearchableText').submit }.to raise_error(Error::UnsupportedOperationError) + element = wait_for_element(name: 'SearchableText') + expect { element.submit }.to raise_error(Error::UnsupportedOperationError) end end @@ -113,7 +117,7 @@ module WebDriver it 'sends key presses' do driver.navigate.to url_for('javascriptPage.html') - key_reporter = driver.find_element(id: 'keyReporter') + key_reporter = wait_for_element(id: 'keyReporter') key_reporter.send_keys('Tet', :arrow_left, 's') expect(key_reporter.attribute('value')).to eq('Test') @@ -122,7 +126,7 @@ module WebDriver # https://github.com/mozilla/geckodriver/issues/245 it 'sends key presses chords', except: {browser: %i[firefox safari safari_preview]} do driver.navigate.to url_for('javascriptPage.html') - key_reporter = driver.find_element(id: 'keyReporter') + key_reporter = wait_for_element(id: 'keyReporter') key_reporter.send_keys([:shift, 'h'], 'ello') expect(key_reporter.attribute('value')).to eq('Hello') @@ -131,7 +135,7 @@ module WebDriver it 'handles file uploads' do driver.navigate.to url_for('formPage.html') - element = driver.find_element(id: 'upload') + element = wait_for_element(id: 'upload') expect(element.attribute('value')).to be_empty path = WebDriver::Platform.windows? ? WebDriver::Platform.windows_path(__FILE__) : __FILE__ @@ -145,7 +149,7 @@ module WebDriver before { driver.navigate.to url_for('formPage.html') } context 'when string type' do - let(:element) { driver.find_element(id: 'checky') } + let(:element) { wait_for_element(id: 'checky') } let(:prop_or_attr) { 'type' } it '#dom_attribute returns attribute value' do @@ -162,7 +166,7 @@ module WebDriver end context 'when numeric type' do - let(:element) { driver.find_element(id: 'withText') } + let(:element) { wait_for_element(id: 'withText') } let(:prop_or_attr) { 'rows' } it '#dom_attribute String' do @@ -179,7 +183,7 @@ module WebDriver end context 'with boolean type of true' do - let(:element) { driver.find_element(id: 'checkedchecky') } + let(:element) { wait_for_element(id: 'checkedchecky') } let(:prop_or_attr) { 'checked' } it '#dom_attribute returns String', except: {browser: :safari} do @@ -211,7 +215,7 @@ module WebDriver end context 'with boolean type of false' do - let(:element) { driver.find_element(id: 'checky') } + let(:element) { wait_for_element(id: 'checky') } let(:prop_or_attr) { 'checked' } it '#dom_attribute returns nil' do @@ -243,7 +247,7 @@ module WebDriver end context 'when property exists but attribute does not' do - let(:element) { driver.find_element(id: 'withText') } + let(:element) { wait_for_element(id: 'withText') } let(:prop_or_attr) { 'value' } it '#dom_attribute returns nil' do @@ -270,7 +274,7 @@ module WebDriver end context 'when attribute exists but property does not' do - let(:element) { driver.find_element(id: 'vsearchGadget') } + let(:element) { wait_for_element(id: 'vsearchGadget') } let(:prop_or_attr) { 'accesskey' } it '#dom_attribute returns attribute' do @@ -287,7 +291,7 @@ module WebDriver end context 'when neither attribute nor property exists' do - let(:element) { driver.find_element(id: 'checky') } + let(:element) { wait_for_element(id: 'checky') } let(:prop_or_attr) { 'nonexistent' } it '#dom_attribute returns nil' do @@ -306,7 +310,7 @@ module WebDriver describe 'style' do before { driver.navigate.to url_for('clickEventPage.html') } - let(:element) { driver.find_element(id: 'result') } + let(:element) { wait_for_element(id: 'result') } let(:prop_or_attr) { 'style' } it '#dom_attribute attribute with no formatting' do @@ -327,7 +331,7 @@ module WebDriver end describe 'incorrect casing' do - let(:element) { driver.find_element(id: 'checky') } + let(:element) { wait_for_element(id: 'checky') } let(:prop_or_attr) { 'nAme' } it '#dom_attribute returns correctly cased attribute' do @@ -344,7 +348,7 @@ module WebDriver end describe 'property attribute case difference with attribute casing' do - let(:element) { driver.find_element(name: 'readonly') } + let(:element) { wait_for_element(name: 'readonly') } let(:prop_or_attr) { 'readonly' } it '#dom_attribute returns a String', except: {browser: :safari} do @@ -361,7 +365,7 @@ module WebDriver end describe 'property attribute case difference with property casing' do - let(:element) { driver.find_element(name: 'readonly') } + let(:element) { wait_for_element(name: 'readonly') } let(:prop_or_attr) { 'readOnly' } it '#dom_attribute returns a String', @@ -381,7 +385,7 @@ module WebDriver end describe 'property attribute name difference with attribute naming' do - let(:element) { driver.find_element(id: 'wallace') } + let(:element) { wait_for_element(id: 'wallace') } let(:prop_or_attr) { 'class' } it '#dom_attribute returns attribute value' do @@ -398,7 +402,7 @@ module WebDriver end describe 'property attribute name difference with property naming' do - let(:element) { driver.find_element(id: 'wallace') } + let(:element) { wait_for_element(id: 'wallace') } let(:prop_or_attr) { 'className' } it '#dom_attribute returns nil' do @@ -415,7 +419,7 @@ module WebDriver end describe 'property attribute value difference' do - let(:element) { driver.find_element(tag_name: 'form') } + let(:element) { wait_for_element(tag_name: 'form') } let(:prop_or_attr) { 'action' } it '#dom_attribute returns attribute value' do @@ -449,14 +453,15 @@ module WebDriver it 'clears' do driver.navigate.to url_for('formPage.html') - expect { driver.find_element(id: 'withText').clear }.not_to raise_error + element = wait_for_element(id: 'withText') + expect { element.clear }.not_to raise_error end it 'gets and set selected' do driver.navigate.to url_for('formPage.html') - cheese = driver.find_element(id: 'cheese') - peas = driver.find_element(id: 'peas') + cheese = wait_for_element(id: 'cheese') + peas = wait_for_element(id: 'peas') cheese.click @@ -471,23 +476,26 @@ module WebDriver it 'gets enabled' do driver.navigate.to url_for('formPage.html') - expect(driver.find_element(id: 'notWorking')).not_to be_enabled + element = wait_for_element(id: 'notWorking') + expect(element).not_to be_enabled end it 'gets text' do driver.navigate.to url_for('xhtmlTest.html') - expect(driver.find_element(class: 'header').text).to eq('XHTML Might Be The Future') + element = wait_for_element(class: 'header') + expect(element.text).to eq('XHTML Might Be The Future') end it 'gets displayed' do driver.navigate.to url_for('xhtmlTest.html') - expect(driver.find_element(class: 'header')).to be_displayed + element = wait_for_element(class: 'header') + expect(element).to be_displayed end describe 'size and location' do it 'gets current location' do driver.navigate.to url_for('xhtmlTest.html') - loc = driver.find_element(class: 'header').location + loc = wait_for_element(class: 'header').location expect(loc.x).to be >= 1 expect(loc.y).to be >= 1 @@ -495,7 +503,7 @@ module WebDriver it 'gets location once scrolled into view' do driver.navigate.to url_for('javascriptPage.html') - loc = driver.find_element(id: 'keyUp').location_once_scrolled_into_view + loc = wait_for_element(id: 'keyUp').location_once_scrolled_into_view expect(loc.x).to be >= 1 expect(loc.y).to be >= 0 # can be 0 if scrolled to the top @@ -503,7 +511,7 @@ module WebDriver it 'gets size' do driver.navigate.to url_for('xhtmlTest.html') - size = driver.find_element(class: 'header').size + size = wait_for_element(class: 'header').size expect(size.width).to be_positive expect(size.height).to be_positive @@ -511,7 +519,7 @@ module WebDriver it 'gets rect' do driver.navigate.to url_for('xhtmlTest.html') - rect = driver.find_element(class: 'header').rect + rect = wait_for_element(class: 'header').rect expect(rect.x).to be_positive expect(rect.y).to be_positive @@ -524,8 +532,8 @@ module WebDriver it 'drags and drop', except: {browser: :ie} do driver.navigate.to url_for('dragAndDropTest.html') - img1 = driver.find_element(id: 'test1') - img2 = driver.find_element(id: 'test2') + img1 = wait_for_element(id: 'test1') + img2 = wait_for_element(id: 'test2') driver.action.drag_and_drop_by(img1, 100, 100) .drag_and_drop(img2, img1) @@ -536,7 +544,7 @@ module WebDriver it 'gets css property' do driver.navigate.to url_for('javascriptPage.html') - element = driver.find_element(id: 'green-parent') + element = wait_for_element(id: 'green-parent') style1 = element.css_value('background-color') style2 = element.style('background-color') # backwards compatibility @@ -548,8 +556,8 @@ module WebDriver it 'knows when two elements are equal' do driver.navigate.to url_for('simpleTest.html') - body = driver.find_element(tag_name: 'body') - xbody = driver.find_element(xpath: '//body') + body = wait_for_element(tag_name: 'body') + xbody = wait_for_element(xpath: '//body') jsbody = driver.execute_script('return document.getElementsByTagName("body")[0]') expect(body).to eq(xbody) From 7b08317a92f622730013427099ef05456e5f0791 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Sat, 6 Dec 2025 21:37:49 +0200 Subject: [PATCH 2/4] speed up Ruby test `element_spec.rb` instead of sleeping for 0.5s, just wait for the needed URL. NB! The previous helper method `wait_for_new_url(old_url)` caused flaky tests because in Firefox, browsers changes URLs like this: "" -> "about:blank" -> "". --- rb/spec/integration/selenium/webdriver/element_spec.rb | 10 +++++----- .../selenium/webdriver/spec_support/helpers.rb | 10 +++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/element_spec.rb b/rb/spec/integration/selenium/webdriver/element_spec.rb index 0d5995eaa1e7e..208e3df80bdcb 100644 --- a/rb/spec/integration/selenium/webdriver/element_spec.rb +++ b/rb/spec/integration/selenium/webdriver/element_spec.rb @@ -59,7 +59,7 @@ module WebDriver driver.navigate.to url_for('formPage.html') wait_for_element(id: 'submitButton').submit - sleep 0.5 + wait_for_url('resultPage.html') expect(driver.title).to eq('We Arrive Here') end @@ -67,7 +67,7 @@ module WebDriver driver.navigate.to url_for('formPage.html') wait_for_element(id: 'checky').submit - sleep 0.5 + wait_for_url('resultPage.html') expect(driver.title).to eq('We Arrive Here') end @@ -75,7 +75,7 @@ module WebDriver driver.navigate.to url_for('formPage.html') wait_for_element(css: 'form > p').submit - sleep 0.5 + wait_for_url('resultPage.html') expect(driver.title).to eq('We Arrive Here') end @@ -83,7 +83,7 @@ module WebDriver driver.navigate.to url_for('formPage.html') wait_for_element(id: 'submit').submit - sleep 0.5 + wait_for_url('resultPage.html') expect(driver.title).to eq('We Arrive Here') end @@ -91,7 +91,7 @@ module WebDriver driver.navigate.to url_for('formPage.html') wait_for_element(name: 'submit').submit - sleep 0.5 + wait_for_url('resultPage.html') expect(driver.title).to eq('We Arrive Here') end diff --git a/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb b/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb index ac2110be4f1f9..84636bf08768c 100644 --- a/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb +++ b/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb @@ -89,11 +89,10 @@ def wait_for_element(locator) wait.until { driver.find_element(locator) } end - def wait_for_new_url(old_url) + def wait_for_url(new_url) wait = Wait.new(timeout: 5) wait.until do - url = driver.current_url - !(url.empty? || url.include?(old_url)) + driver.current_url.include?(new_url) end end @@ -102,6 +101,11 @@ def wait_for_devtools_target(target_type:) wait.until { driver.devtools(target_type: target_type).target } end + def wait_for_title(title:) + wait = Wait.new(timeout: 5) + wait.until { driver.title == title } + end + def wait(timeout = 10) Wait.new(timeout: timeout) end From cbe794acdd6e2ea3d99888d141db67a4d7b1c296 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Sat, 6 Dec 2025 22:43:17 +0200 Subject: [PATCH 3/4] speed up Ruby test `devtools_spec.rb` instead of sleeping for 2.5s, just wait for the needed logs. --- .../selenium/webdriver/devtools_spec.rb | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/devtools_spec.rb b/rb/spec/integration/selenium/webdriver/devtools_spec.rb index 6b75778d0bb04..2427c8f71ccfb 100644 --- a/rb/spec/integration/selenium/webdriver/devtools_spec.rb +++ b/rb/spec/integration/selenium/webdriver/devtools_spec.rb @@ -100,26 +100,20 @@ module WebDriver it 'notifies about log messages' do logs = [] - driver.on_log_event(:console) { |log| logs.push(log) } + driver.on_log_event(:console) { |log| logs.push(log.args[0]) } driver.navigate.to url_for('javascriptPage.html') - driver.execute_script("console.log('I like cheese');") - sleep 0.5 driver.execute_script('console.log(true);') - sleep 0.5 driver.execute_script('console.log(null);') - sleep 0.5 driver.execute_script('console.log(undefined);') - sleep 0.5 driver.execute_script('console.log(document);') - sleep 0.5 + driver.execute_script("console.log('I like cheese');") - expect(logs).to include( - an_object_having_attributes(type: :log, args: ['I like cheese']), - an_object_having_attributes(type: :log, args: [true]), - an_object_having_attributes(type: :log, args: [nil]), - an_object_having_attributes(type: :log, args: [{'type' => 'undefined'}]) - ) + wait.until { logs.include?('I like cheese') } + + expect(logs).to include(true) + expect(logs).to include(nil) + expect(logs).to include({'type' => 'undefined'}) end it 'notifies about document log messages' do From 6efb912282b713ef8f2efa2ee576504ca0c08da5 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Mon, 8 Dec 2025 00:45:59 +0200 Subject: [PATCH 4/4] add page url and source if some element was not found --- rb/lib/selenium/webdriver/common/wait.rb | 5 ++++- rb/spec/integration/selenium/webdriver/element_spec.rb | 2 +- .../selenium/webdriver/spec_support/helpers.rb | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/rb/lib/selenium/webdriver/common/wait.rb b/rb/lib/selenium/webdriver/common/wait.rb index abe3889f8c02b..90e6df39b7320 100644 --- a/rb/lib/selenium/webdriver/common/wait.rb +++ b/rb/lib/selenium/webdriver/common/wait.rb @@ -37,7 +37,8 @@ def initialize(opts = {}) @timeout = opts.fetch(:timeout, DEFAULT_TIMEOUT) @interval = opts.fetch(:interval, DEFAULT_INTERVAL) @message = opts[:message] - @ignored = Array(opts[:ignore] || Error::NoSuchElementError) + @message_provider = opts[:message_provider] + @ignored = Array(opts[:ignore] || Error::NoSuchElementError) end # @@ -64,6 +65,8 @@ def until msg = if @message @message.dup + elsif @message_provider + @message_provider.call else "timed out after #{@timeout} seconds" end diff --git a/rb/spec/integration/selenium/webdriver/element_spec.rb b/rb/spec/integration/selenium/webdriver/element_spec.rb index 208e3df80bdcb..06d1e04a43674 100644 --- a/rb/spec/integration/selenium/webdriver/element_spec.rb +++ b/rb/spec/integration/selenium/webdriver/element_spec.rb @@ -32,7 +32,7 @@ module WebDriver # Safari returns "click intercepted" error instead of "element click intercepted" it 'raises if different element receives click', except: {browser: %i[safari safari_preview]} do driver.navigate.to url_for('click_tests/overlapping_elements.html') - element = wait_for_element(id: 'contents') + element = wait_for_element(id: 'contents', timeout: 10) expect { element.click }.to raise_error(Error::ElementClickInterceptedError) end diff --git a/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb b/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb index 84636bf08768c..a3a7f0ac11160 100644 --- a/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb +++ b/rb/spec/integration/selenium/webdriver/spec_support/helpers.rb @@ -84,8 +84,12 @@ def wait_for_no_alert wait.until { driver.title } end - def wait_for_element(locator) - wait = Wait.new(timeout: 25, ignore: Error::NoSuchElementError) + def wait_for_element(locator, timeout = 25) + wait = Wait.new(timeout: timeout, ignore: Error::NoSuchElementError, message_provider: lambda { + url = "page url: #{driver.current_url};\n" + source = "page source: #{driver.find_element(css: 'body').attribute('innerHTML')}\n" + "could not find element #{locator} in #{timeout} seconds;\n#{url}#{source}" + }) wait.until { driver.find_element(locator) } end