Refactor Selenium Connection Logic#64045
Conversation
Extract some common functionality from the SauceLabs-specific Selenium logic into our generic Selenium helper, and streamline and clean up some miscellaneous associated code. All in preparation for [eventually adding an alternate remote Selenium browser](#63606)
| capabilities["sauce:options"] ||= {} | ||
| capabilities["sauce:options"].merge!(sauce_options) | ||
|
|
||
| very_verbose "DEBUG: Capabilities: #{CGI.escapeHTML capabilities.inspect}" |
There was a problem hiding this comment.
I did not preserve this very_verbose method call; it is here for the purposes of extra debug logging, and as far as I can tell has not been useful for quite some time. Happy to put it back in if folks would prefer; just let me know
There was a problem hiding this comment.
This is the type of print statement I think is honestly easier just to insert yourself if/when you need it, custom to your specific needs.
|
|
||
| def get_browser(test_run_name) | ||
| browser = nil | ||
| $http_client ||= SeleniumBrowser::Client.new(read_timeout: 2.minutes) |
There was a problem hiding this comment.
This does have a side effect of making this variable available when using the local browser, when formerly it was only available when using the remote browser. As far as I can tell, this does not impact the experience of running tests locally in any way, and doing it this way makes it a bit cleaner to add an alternative remote browser. Let me know if you have any concerns about the change, and I can find a solution that more thoroughly preserves existing behavior.
There was a problem hiding this comment.
totally fine by me, simple + easy-to-search => good
There was a problem hiding this comment.
I find $globals are relatively innocuous when all refs are same-file, esp when well namespaced.
- There's only reference to $http_client outside connect.rb, in steps.rb! It could easily be replaced by a
def change_orientation()method in connect.rb that is invoked in steps.rb. The same pattern is already used by with_read_timeout() in steps.rb which is invoked in steps.rb and thereby hides the $http_client. Then $http_client would only be ref-ed same-file, woot. - This one could be more namespaced: e.g.
$selenium_http_client. ick, but imo global is not the time to be picky about terse and clean to read.
The above two changes would make me happier, but I'm not unhappy as-is either.
| end | ||
|
|
||
| def to_percent(number, n_sig_digits) | ||
| return nil if number.nil? |
There was a problem hiding this comment.
this check is necessary to avoid throwing an exception if flakiness_for_test returns nil for whatever reason. I don't think we want to block our ability to run the test in the event we don't have flakiness data
| $browser_config = JSON.parse(File.read(File.join(UI_TEST_DIR, 'browsers.json'))).detect {|b| b['name'] == ENV['BROWSER_CONFIG']} || {} | ||
|
|
||
| MAX_CONNECT_RETRIES = 3 | ||
| SAUCELABS_SELENIUM_URL = 'https://ondemand.us-west-1.saucelabs.com/wd/hub'.freeze |
There was a problem hiding this comment.
I wouldn't hate if this was over-rideable by an env var. Not a must-have, but would allow say a 3rd party to use their suacelabs endpoint which happened to be on us-east-1 or whatever.
|
|
||
| def get_browser(test_run_name) | ||
| browser = nil | ||
| $http_client ||= SeleniumBrowser::Client.new(read_timeout: 2.minutes) |
There was a problem hiding this comment.
totally fine by me, simple + easy-to-search => good
|
|
||
| def get_browser(test_run_name) | ||
| browser = nil | ||
| $http_client ||= SeleniumBrowser::Client.new(read_timeout: 2.minutes) |
There was a problem hiding this comment.
I find $globals are relatively innocuous when all refs are same-file, esp when well namespaced.
- There's only reference to $http_client outside connect.rb, in steps.rb! It could easily be replaced by a
def change_orientation()method in connect.rb that is invoked in steps.rb. The same pattern is already used by with_read_timeout() in steps.rb which is invoked in steps.rb and thereby hides the $http_client. Then $http_client would only be ref-ed same-file, woot. - This one could be more namespaced: e.g.
$selenium_http_client. ick, but imo global is not the time to be picky about terse and clean to read.
The above two changes would make me happier, but I'm not unhappy as-is either.
|
|
||
| module SeleniumBrowser | ||
| def self.webdriver_options_object(browser: :chrome, headless: false) | ||
| options = Selenium::WebDriver::Options.send(browser, args: ['window-size=1280,1024']) |
There was a problem hiding this comment.
- I like+prefer the simplification you've done here with
window-size=1280,1024always being passed in. - This /does/, technically, I think, change options on Safari (which might ignore, no idea), and probably more worrying what happens on stuff like iPad? Probably a no-op?
- I'm still consistently confused by how browsers.json aka $browser_config intersects (or doesn't??), which also has resolution related fields, but that's kind of a me problem:
{
"name": "Chrome",
"browserName": "chrome",
"browserVersion": "105",
"platformName": "Windows 10",
"sauce:options": {
"screenResolution": "1280x1024",
"extendedDebugging": true
}
},
I suspect there's a simplification to be had here, somewhere, if only because I can't quite grok what we have now lol.
There was a problem hiding this comment.
OK, so unwinding this just for myself if for no other reason:
capabilities:inSeleniumBrowser.remote(url, capabilities: nil)below is generally ~~= $browser_config- because
SeleniumBrowser.remoteis only called fromdef saucelabs_browser()in connect.rb - which sets
capabilities = Selenium::WebDriver::Remote::Capabilities.new($browser_config.except('name')), and then more-or-less just sets a locally definedcapabilities[:sauce_options]and passes $browser_config. - And $browser_config == browser.json
Would it be crazy to add another method to SeleniumBrowser called SeleniumBrowser.remote_saucelabs, which invokes self.remote, and refactors the sauce-labs specific capabilities munging code (and loading browser.json) out of connect.rb?
There was a problem hiding this comment.
This /does/, technically, I think, change options on Safari (which might ignore, no idea), and probably more worrying what happens on stuff like iPad?
Oh, good catch; I figured making Safari more consistent with the other browsers was a positive, but I wasn't thinking at all about mobile devices. I think I'll scope this down to just if [:chrome, :firefox].include?(browser) for now.
The relationship between this setting and the one in browsers.json is also unclear to me; in particular, it's not clear to me whether SauceLabs would be able to default to the resolution defined in Selenium if we don't explicitly specify a resolution in sauce:options.
Would it be crazy to add another method to SeleniumBrowser called
SeleniumBrowser.remote_saucelabs[...]
When I was first putting this together, I spent a while standing at the precipice of the $browser_config / $http_client rabbit hole, debating diving in.
The main reason I decided not to for $browser_config is that I'd then also have to untangle the implementation and use of the single_session? helper method, which felt like too much for this PR. I'd be strongly in favor of us doing some cleanup there as a follow-up, but I wanted to keep this one at least somewhat focused. I also liked the idea of keeping all code that referenced ENV variables in one place, and keeping the code in selenium_browser.rb a bit more environment-agnostic.
I could be much more easily convinced to fold some $http_client improvements in here, though; it was a little annoying to accommodate in this refactor, and I didn't realize it was only externally referenced from a single place 👀
stephenliang
left a comment
There was a problem hiding this comment.
I wonder if we should also toss in a selenium gem upgrade to enable modern chrome versions? I believe we are pinned to a specific version, but perhaps that is a local only issue and the remote grid would still be able to support modern chrome despite local not supporting it? Either way, this comment is out of scope for this PR
|
@stephenliang I'd love that too, but maybe a quick followup PR? just because I could imagine this PR breaking the test server in ways it didn't on drone (namely: ipads etc), and I could imagine that for a selenium gem upgrade too. May or may not be related, but upgrading eyes_selenium is not a version-bump-and-done: #61802 |
|
Agree with separate PR for that |
We currently have a
SeleniumBrowserhelper module which defines a methodlocalwhich will return a Selenium WebDriver object configured for running tests locally. We also have a standalone helper methodsaucelabs_browserdefined inconnect.rbwhich will return a Selenium WebDriver object configured for running remote tests against SauceLabs.In preparation for some upcoming work which will add an alternative to SauceLabs for remote testing, I extract some of the functionality from
saucelabs_browserinto a new methodremoteon theSeleniumBrowserhelper module, and some of the shared logic from both the oldlocaland the newremotemethod to a newwebdriver_options_objecthelper method. Anything that is specific to SauceLabs rather than generically Selenium I left inconnect.rb.Also cleaned up a few miscellaneous implementation details while I was in the area.
Links / Follow-up work
All in preparation for eventually adding an alternate remote Selenium browser
Testing story
Relying entirely on existing tests to verify that this test-logic-only change does not change any existing functionality.