Why Selenium Clicks Fail

Maintaining a stable Selenium test suite is a time-consuming endeavor, and Selenium scripts can fail in nondeterministic and mysterious ways. This is a huge problem across the in the testing industry, and at Lucid we pride ourselves in being on the cutting edge of E2E automation. While automation is a task that all engineers at Lucid participate in, we maintain a team of dedicated automation engineers to help guide the process, create documentation, and teach best practices. We’ve found that one of the biggest sources of failures is clicks failing to behave as expected and have discovered many of the reasons by detailed diagnosis of our own Selenium test failures. We’ve aggregated the reasons for these failures, and they’re applicable for many websites and not just our own. Below we’ve compiled some different ways Selenium clicks fail and how to address each challenge.


Basic button

The button below displays an alert when clicked.

Testing the button click with Selenium is straightforward.

def example(driver: RemoteWebDriver): Unit = {
  val html = """<button onclick='alert("Clicked");'>Button</button>"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  button.click()
  driver.switchTo().alert()
}

(We use Scala here, but the API is similar in other languages.)


Stale button

Selenium clicks fail when an element is not attached to the current page. Re-find the element if the previously found element has been detached.

def staleExample(driver: RemoteWebDriver): Unit = {
  val html = "<button>Button</button>"
  driver.get("data:text/html;charset=utf-8," + html)
  var button = driver.findElementByTagName("button")
  driver.navigate().refresh()
  button = driver.findElementByTagName("button")
  button.click() // throws StaleElementReferenceException if re-finding is removed
}

Disabled button

Disabled buttons don’t fire click events. A wait condition can be used if the button eventually transitions to enabled.

Example:
def initiallyDisabledExample(driver: RemoteWebDriver): Unit = {
  val html = """
<button onclick='alert();' disabled>Initially disabled button</button>
<script>
  const button = document.querySelector('button');
  setTimeout(() => button.disabled = false, 1000);
</script>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  new WebDriverWait(driver, 3).until(ExpectedConditions.elementToBeClickable(button))
  button.click()
  driver.switchTo().alert() // throws NoAlertPresentException if wait is removed
}

Hidden button

Invisible elements cannot be clicked. Wait until elements become visible.

Example:
def initiallyInvisibleExample(driver: RemoteWebDriver): Unit = {
  val html = """
<button style='visibility:hidden'>Initially hidden button</button>
<script>
  const button = document.querySelector('button');
  setTimeout(() => button.style.visibility = 'visible', 1000);
</script>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  new WebDriverWait(driver, 3).until(ExpectedConditions.visibilityOf(button))
  button.click() // throws ElementNotInteractableException if wait is removed
}

(Older versions threw ElementNotVisibleException instead.)


Covered button

Overlapping elements intercept clicks from overlapped ones. Wait for the element to be uncovered.

Example:
Overlapping element
def initiallyCoveredExample(driver: RemoteWebDriver): Unit = {
  val html = """
<button>Initially covered button</button>
<span style='position:absolute;left:0px'>Overlapping</span>
<script>
  const span = document.querySelector('span');
  setTimeout(() => span.style.visibility = 'hidden', 1000);
</script>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  val span = driver.findElementByTagName("span")
  new WebDriverWait(driver, 3).until(ExpectedConditions.invisibilityOf(span))
  button.click() // throws ElementClickInterceptedException if wait is removed
}

(Older versions threw WebDriverException: Element is not clickable.)


Moving button

Firefox’s geckodriver reliably clicks moving elements. ChromeDriver requires extra work.

ChromeDriver can’t click a moving element

This is not a supported feature of ChromeDriver. If the element eventually stops, wait for that to occur.

https://chromedriver.chromium.org/help/clicking-issues
Example:
def moveRightExample(driver: ChromeDriver): Unit = {
  val html = """
<button onclick='alert();' style='position: relative;animation: moveRight 1s forwards'>This button moves right</button>
<style>
  @keyframes moveRight {
    from {left: 0px;}
    to {left: 500px;}
  }
</style>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  def notMoving(element: WebElement) = new ExpectedCondition[Boolean] {
    override def apply(input: WebDriver): Boolean = {
      val startLocation = element.getLocation
      Thread.sleep(500)
      val endLocation = element.getLocation
      startLocation == endLocation
    }
  }
  new WebDriverWait(driver, 3, 0).until(notMoving(button))
  button.click()
  driver.switchTo().alert()
}

If the element never stops moving, ChromeDriver does not guarantee the click will be successful.

https://chromedriver.chromium.org/help/clicking-issues

Sending an ‘Enter’ key directly to an element simulates a click.  (Of course, clicking and pressing ‘Enter’ aren’t exactly the same but may be good enough).

def movingExample(driver: ChromeDriver): Unit = {
  val html = """
<button onclick='alert();' style='position: relative;animation: moveRight 2s infinite alternate'>Moving button</button>
<style>
  @keyframes moveRight {
    from {left: 0px;}
    to {left: 500px;}
  }
</style>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  button.sendKeys(Keys.ENTER)
  driver.switchTo().alert()
}

This avoids the race condition in ChromeDriver’s click implementation between calculating an element’s location and sending mousedown/mouseup events at that location. (Failure due to this race condition may result in ElementClickInterceptedException or mousedown/mouseup events getting delivered to the wrong elements.)


Resizing button

Because Selenium attempts to click the center of elements, an element that changes size adjusts the click location and creates similar pitfalls in ChromeDriver to that of a moving element. Wait for the size to stabilize.

Example:
def shrinkExample(driver: ChromeDriver): Unit = {
  val html = """
<button onclick='alert();' style='animation: shrink 1s forwards'>This button shrinks</button>
<style>
  @keyframes shrink {
    from {width: 750px;}
    to {width: 75px;}
  }
</style>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  def sizeStabilizes(element: WebElement) = new ExpectedCondition[Boolean] {
    override def apply(input: WebDriver): Boolean = {
      val startSize = element.getSize
      Thread.sleep(500)
      val endSize = element.getSize
      startSize == endSize
    }
  }
  new WebDriverWait(driver, 3, 0).until(sizeStabilizes(button))
  button.click()
  driver.switchTo().alert()
}

The same ‘Enter’ trick for moving elements can be used with elements that change size.

def resizingExample(driver: ChromeDriver): Unit = {
  val html = """
<button onclick='alert();' style='animation: shrink 2s infinite alternate'>Resizing button</button>
<style>
  @keyframes shrink {
    from {width: 750px;}
    to {width: 75px;}
  }
</style>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  val button = driver.findElementByTagName("button")
  button.sendKeys(Keys.ENTER)
  driver.switchTo().alert()
}

Refreshing button

Elements that are periodically refreshed can separate Selenium’s find and/or click events. Executing Javascript to perform the find and click prevents separation.

def refreshingExample(driver: RemoteWebDriver): Unit = {
  val html = """
<button onclick='alert();'>This button is frequently refreshed</button>
<script>
  function refreshButton() {
    const button = document.querySelector('button');
    const clone = button.cloneNode(true);
    button.replaceWith(clone);
  }
  setInterval(refreshButton, 1000);
</script>
"""
  driver.get("data:text/html;charset=utf-8," + html)
  driver.executeScript("document.querySelector('button').click();")
  driver.switchTo().alert()
}

(Admittedly, a click through Javascript doesn’t perfectly simulate a user click, but it’s a simple partial solution.)


Hopefully this article helps resolve some of your own Selenium clicking woes. More obscure types of click failures occasionally occur here at Lucid, but most fall into one of the categories described above.

No Comments, Be The First!

Your email address will not be published.