A button click in Selenium has to happen after the browser exposes the element as visible and enabled. Clicking too early can produce no click, an intercepted click, or a stale element error after the page redraws, so the test should wait for the exact control and verify the page state caused by the click.
In Python, the usual flow is a locator tuple, WebDriverWait, EC.element_to_be_clickable(), and click() on the returned WebElement. A locator-based wait keeps the lookup close to the action, which matters when JavaScript enables or replaces the button after the initial page load.
Use this pattern for normal page buttons, links, and controls that a user could click. If an overlay covers the button, the button stays disabled, a native alert is open, or the button lives inside an iframe, fix that browser state or switch context before clicking instead of hiding the timing problem behind time.sleep() or a JavaScript click.
from pathlib import Path from tempfile import TemporaryDirectory import shutil from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait HTML = """<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Selenium button click demo</title> <script> window.addEventListener("DOMContentLoaded", () => { const button = document.querySelector("#place-order"); const status = document.querySelector("#status"); setTimeout(() => { button.disabled = false; status.textContent = "ready"; }, 300); button.addEventListener("click", () => { status.textContent = "clicked"; button.dataset.clicked = "true"; }); }); </script> </head> <body> <button id="place-order" disabled>Place order</button> <p id="status">waiting</p> </body> </html> """ options = Options() options.add_argument("--headless=new") options.add_argument("--window-size=1280,720") for browser_name in ("google-chrome", "chromium", "chromium-browser"): browser_path = shutil.which(browser_name) if browser_path: options.binary_location = browser_path break driver_path = shutil.which("chromedriver") service = Service(driver_path) if driver_path else Service() with TemporaryDirectory() as tmpdir: page = Path(tmpdir) / "button-click.html" page.write_text(HTML, encoding="utf-8") driver = webdriver.Chrome(service=service, options=options) try: driver.get(page.as_uri()) wait = WebDriverWait(driver, 10) button = wait.until(EC.element_to_be_clickable((By.ID, "place-order"))) button_text = button.text button.click() wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "clicked")) status = driver.find_element(By.ID, "status").text print(f"title: {driver.title}") print(f"button_text: {button_text}") print(f"status: {status}") finally: driver.quit()
The demo waits for a disabled button to become clickable, clicks it, and checks the page text changed after the click.
$ python3 selenium-click-button.py title: Selenium button click demo button_text: Place order status: clicked
checkout_button = wait.until( EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-testid='checkout']")) ) checkout_button.click()
Prefer a stable id or test attribute when the application provides one. Button text and generated CSS class names often change before the click behavior changes.
wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='order-confirmation']")) )
A successful click() call only proves Selenium sent the action. The follow-up wait proves the application reacted to it.
button = wait.until(EC.element_to_be_clickable((By.ID, "place-order"))) button.click()
A saved WebElement can become stale after JavaScript replaces the node. Keep the locator and wait for a fresh element before the next click.
Related: How to troubleshoot stale element errors in Selenium
$ rm selenium-click-button.py