I recently noticed a new kind of flaky tests on the slow free tier GitHub Action runners: Integration tests were running on smaller screen sizes than specified in the device metrics. The root cause was the use of Selenium's page.driver.resize_window_to
methods, which
by design
Show archive.org snapshot
does not block until the resizing process has settled:
We discussed this issue again recently, and agreed that windows size manipulation operations are asynchronous by nature, because we can't control window rendering effects added by a window manager, and browsers don't provide a callback that would allow to detect the end of windows size changing process.
Capybara provides a better way to manipulate the browser window which you should use instead: page.current_window.resize_to
.
Good: Capybara::Window#resize_to
page.current_window.resize_to(width, height)
Reason
Capybara#Window#resize_to
blocks until the window size equals the expected dimensions.
# Source code of #resize_to for additional context:
class Capybara::Window
def resize_to(width, height)
wait_for_stable_size { @driver.resize_window_to(handle, width, height) }
end
def wait_for_stable_size(seconds = session.config.default_max_wait_time)
res = yield if block_given?
timer = Capybara::Helpers.timer(expire_in: seconds)
loop do
prev_size = size
sleep 0.025
return res if prev_size == size
break if timer.expired?
end
raise Capybara::WindowError, "Window size not stable within #{seconds} seconds."
end
end
Bad: Capybara::Selenium::Driver#resize_window_to
page.driver.resize_window_to(page.driver.current_window_handle, width, height)
Reason
It's the same as above, but without a hard requirement for the resizing to settle. It will wait for at most 250ms before moving on.
# Source code of #resize_window_to for additional context:
class Capybara::Selenium::Driver
def resize_window_to(handle, width, height)
within_given_window(handle) do
browser.manage.window.resize_to(width, height)
end
rescue Selenium::WebDriver::Error::UnknownError => e
raise unless e.message.include?('failed to change window state')
# Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
# and raises unnecessary error. Wait a bit and try again.
sleep 0.25
within_given_window(handle) do
browser.manage.window.resize_to(width, height)
end
end
end
class Selenium::WebDriver::Window
def resize_to(width, height)
@bridge.resize_window Integer(width), Integer(height)
end
end
class Selenium::WebDriver::Remote::Bridge
def resize_window(width, height, handle = :current)
raise Error::WebDriverError, 'Switch to desired window before changing its size' unless handle == :current
# Chromedriver API call named "set_window_rect"
set_window_rect(width: width, height: height)
end
end
A note on device emulations
We usually define multiple device emulations with different screen resolutions in our Capybara config:
Capybara.register_driver(:desktop) { ... }
Capybara.register_driver(:tablet) { ... }
Capybara.register_driver(:mobile) { ... }
If you resize your entire spec using the driver: :tablet
tag, you should be fine as we didn't notice any issues yet. Otherwise you could resize your window before every example just to be sure:
RSpec.configure do |config|
config.before(type: :feature) do
case Capybara.current_driver
when :desktop
resize_browser_to(DESKTOP_WIDTH, DESKTOP_HEIGHT)
when :tablet
resize_browser_to(TABLET_WIDTH, TABLET_HEIGHT)
when :mobile
resize_browser_to(MOBILE_WIDTH, MOBILE_HEIGHT)
else
# Nothing to do in case of rack tests
end
end
end
This could however reduce your test performance.