When unit testing using Python and Selenium, it’s usually easiest to click a button with an element’s ID, but I’ve found a more flexible solution that doesn’t require multiple different function calls to be best.
To that end, here’s a method I’ve written that one can use to click a button using one of three identifiers for an element:
- A partial but still unique ID. (I test a site with long, dynamically generated IDs, where usually only the ending portion of the ID is relevant.)
- A CSS selector.
- An XPath.
Depending on a website’s HTML structure and the specific actions one is trying to take, sometimes only an XPath or CSS selector will really do the trick, but it’s nice to be able to just copy some or all of an ID when available.
For context, this is part of a class called SeleniumHelper, which has a Selenium WebDriver instance stored in self.driver
. Here’s the full method to start; we’ll go through it in detail below.
def click_button(self, identifier, dbl_click=False): attempts = 0 while True: try: element = self.driver.find_element_by_xpath( f"//*[contains(@id, '{identifier}')]" ) except NoSuchElementException: try: # try to find the element by CSS selector element = self.driver.find_element_by_css_selector( f"{identifier}" ) except NoSuchElementException: try: # try to find element by xpath element = self.driver.find_element_by_xpath( f"{identifier}" ) except NoSuchElementException: print( f"Cannot find element by ID, CSS selector, or " f"xpath:\n\n\t{identifier}\n\nPlease send a valid " f"selection string of one of these types." ) attempts += 1 if attempts > 5: break if dbl_click: action = ActionChains(self.driver) action.double_click(element).perform() else: action = ActionChains(self.driver) action.move_to_element(element).click(element).perform() break
Selecting the Element
The bulk of this method is pretty much just working through the selection of an element as flexibly as possible.
First, we check for a partial but unique ID attribute:
try: element = self.driver.find_element_by_xpath( f"//*[contains(@id, '{identifier}')]" )
If that fails, we’re looking for an element by CSS selector:
except NoSuchElementException: try: # try to find the element by CSS selector element = self.driver.find_element_by_css_selector( f"{identifier}" )
As a last resort, we look for the element by XPath:
except NoSuchElementException: try: # try to find element by xpath element = self.driver.find_element_by_xpath( f"{identifier}" )
Giving Elements a Chance to Load
The problem I’ve found with using Selenium’s WebDriverWait class within this function is that it will wait for the ID check, then the CSS selector, then the XPath, really slowing up the testing process. Instead, I’ve wrapped these selectors in a while
statement that counts the number of attempts through the process. I have it set to five attempts here, but that can be increased or decreased depending on a site’s load speed or other factors:
def click_button(self, identifier, dbl_click=False): attempts = 0 while True: try: # selections all happen here # ... etc. etc. etc. # if not found by any of the three selection types: except NoSuchElementException: attempts += 1 if attempts > 5: print( f"Cannot find element by ID, CSS selector, or " f"xpath:\n\n\t{identifier}\n\nPlease send a " f"valid selection string of one of these types." ) break
This isn’t a perfect solution, as sometimes elements will take longer to load than the number of attempts specified will take to perform. The speed gains, in my opinion, are worth the inconvenience of having to code waits outside of this function call.
In truth, this portion of the function could probably be handled more elegantly and reliably, but it has saved me from a few errors without slowing down testing significantly. If you’re positive an element will appear, you could simply have the loop continue until the element is found, not counting attempts at all. The problem with this approach is that if something goes wrong with the webpage, a test can be caught in an endless loop until the user notices.
Clicking the Button
You probably noticed the dbl_click=False
in the function definition:
def click_button(self, identifier, dbl_click=False):
By default, calling click_button(identifier)
will single-click the element. By instead specifying in the function call that dbl_click=True
(i.e., click_button(identifier, dbl_click=True)
), the element will instead be double-clicked. This isn’t often necessary on webpages, but it’s nice to be able to easily call it with an optional argument.
if dbl_click: action = ActionChains(self.driver) action.double_click(element).perform() else: action = ActionChains(self.driver) action.move_to_element(element).click(element).perform() break
Don’t forget to break
out of the while
statement, or Selenium will try to click the same button forever.