Skip to content

Refactor Selenium Connection Logic#64045

Merged
Hamms merged 6 commits into
stagingfrom
elijah/refactor-selenium-connect
Mar 3, 2025
Merged

Refactor Selenium Connection Logic#64045
Hamms merged 6 commits into
stagingfrom
elijah/refactor-selenium-connect

Conversation

@Hamms

@Hamms Hamms commented Feb 19, 2025

Copy link
Copy Markdown
Contributor

We currently have a SeleniumBrowser helper module which defines a method local which will return a Selenium WebDriver object configured for running tests locally. We also have a standalone helper method saucelabs_browser defined in connect.rb which 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_browser into a new method remote on the SeleniumBrowser helper module, and some of the shared logic from both the old local and the new remote method to a new webdriver_options_object helper method. Anything that is specific to SauceLabs rather than generically Selenium I left in connect.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.

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}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally fine by me, simple + easy-to-search => good

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find $globals are relatively innocuous when all refs are same-file, esp when well namespaced.

  1. 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.
  2. 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?

@Hamms Hamms Feb 19, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@Hamms Hamms marked this pull request as ready for review February 20, 2025 19:12
@Hamms Hamms requested review from a team February 20, 2025 19:12

@snickell snickell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very like the simplification, TY. I think this PR would improve the codebase as-is, thus green check. There's a few small things I'd prefer, but very no big deal to ignore.

$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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find $globals are relatively innocuous when all refs are same-file, esp when well namespaced.

  1. 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.
  2. 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'])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I like+prefer the simplification you've done here with window-size=1280,1024 always being passed in.
  2. 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?
  3. 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so unwinding this just for myself if for no other reason:

  1. capabilities: in SeleniumBrowser.remote(url, capabilities: nil) below is generally ~~= $browser_config
  2. because SeleniumBrowser.remote is only called from def saucelabs_browser() in connect.rb
  3. which sets capabilities = Selenium::WebDriver::Remote::Capabilities.new($browser_config.except('name')), and then more-or-less just sets a locally defined capabilities[:sauce_options] and passes $browser_config.
  4. 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?

@Hamms Hamms Feb 27, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stephenliang left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@snickell

Copy link
Copy Markdown
Collaborator

@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

@stephenliang

Copy link
Copy Markdown
Member

Agree with separate PR for that

@Hamms Hamms merged commit 844d069 into staging Mar 3, 2025
@Hamms Hamms deleted the elijah/refactor-selenium-connect branch March 3, 2025 23:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants