Skip to main content
Selenium beginner Lesson 4 of 10

Interacting with Elements and Forms

Type, click, select and upload — plus ActionChains for hover and drag, and what to do when a cookie banner intercepts your click.

Most interaction is click() and send_keys(). The rest of this lesson is the cases where those are not enough, and the failures they produce when the page is not what you assumed.

A form to work with

FORM = """data:text/html,
<form id="signup">
  <label for="email">Email</label><input id="email" type="email">
  <label for="plan">Plan</label>
  <select id="plan">
    <option value="free">Free</option>
    <option value="pro">Pro</option>
    <option value="team">Team</option>
  </select>
  <label><input id="terms" type="checkbox"> Accept terms</label>
  <label><input name="contact" type="radio" value="email" checked> Email</label>
  <label><input name="contact" type="radio" value="sms"> SMS</label>
  <textarea id="notes"></textarea>
  <button type="submit">Create account</button>
  <p id="result"></p>
</form>
<script>
signup.onsubmit = e => {
  e.preventDefault();
  result.textContent = [email.value, plan.value, terms.checked, signup.contact.value,
                        notes.value].join(' | ');
};
</script>"""

data: URLs let these examples run without a server.

Typing and clicking

from selenium.webdriver.support.ui import Select

with webdriver.Chrome(options=opts) as driver:
    driver.get(FORM)

    email = driver.find_element(By.ID, "email")
    email.send_keys("ada@example.com")

    Select(driver.find_element(By.ID, "plan")).select_by_value("pro")
    driver.find_element(By.ID, "terms").click()
    driver.find_element(By.CSS_SELECTOR, "input[value='sms']").click()
    driver.find_element(By.ID, "notes").send_keys("deliver after 6pm")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    print(driver.find_element(By.ID, "result").text)
ada@example.com | pro | true | sms | deliver after 6pm

send_keys appends — it does not replace. To overwrite an existing value:

    email.clear()
    email.send_keys("grace@example.com")
    print(email.get_attribute("value"))
grace@example.com

clear() fails on some framework-controlled inputs, because it does not always fire the events the component listens for. The reliable fallback is a select-all:

    from selenium.webdriver.common.keys import Keys
    email.send_keys(Keys.CONTROL + "a")
    email.send_keys("alan@example.com")
    print(email.get_attribute("value"))
alan@example.com

Selects

    plan = Select(driver.find_element(By.ID, "plan"))

    plan.select_by_value("team")
    print(plan.first_selected_option.text)
    plan.select_by_visible_text("Pro")
    print(plan.first_selected_option.get_attribute("value"))
    plan.select_by_index(0)
    print(plan.first_selected_option.text)
    print([o.text for o in plan.options])
Team
pro
Free
['Free', 'Pro', 'Team']

Select only works on a native <select>. A React or Vue “dropdown” built from divs raises:

selenium.common.exceptions.UnexpectedTagNameException: Message: Select only works on
<select> elements, not on <div>

For those, it is two clicks — open the control, then click the option:

    driver.find_element(By.CSS_SELECTOR, "[data-testid='plan-trigger']").click()
    wait.until(EC.element_to_be_clickable((By.XPATH, "//li[text()='Pro']"))).click()

Checkboxes and radios

    terms = driver.find_element(By.ID, "terms")
    print("before:", terms.is_selected())
    if not terms.is_selected():
        terms.click()
    print("after: ", terms.is_selected())
before: False
after:  True

There is no check()click() toggles. Guarding with is_selected() makes the step idempotent, which matters when a test re-enters a form or a retry replays the step.

The click that gets intercepted

OVERLAY = """data:text/html,
<button id="save" onclick="result.textContent='saved'">Save</button>
<div style="position:fixed;inset:0;background:rgba(0,0,0,.5)">Cookie banner</div>
<p id="result"></p>"""

with webdriver.Chrome(options=opts) as driver:
    driver.get(OVERLAY)
    driver.find_element(By.ID, "save").click()
selenium.common.exceptions.ElementClickInterceptedException: Message: element click
intercepted: Element <button id="save">...</button> is not clickable at point (412, 380).
Other element would receive the click:
<div style="position:fixed;inset:0;background:rgba(0,0,0,.5)">...</div>
  (Session info: chrome=133.0.6943.16)

The error names the element that stole the click, which is unusually helpful. Three responses, in order of preference:

# 1. deal with the overlay — what a user would do
driver.find_element(By.CSS_SELECTOR, ".cookie-banner .accept").click()
driver.find_element(By.ID, "save").click()

# 2. scroll it into view, when the blocker is a sticky header or footer
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", button)
button.click()

# 3. last resort — bypass the browser entirely
driver.execute_script("arguments[0].click();", button)
saved

The third works and hides the bug. A JavaScript click dispatches the event directly, ignoring visibility, overlays and disabled styling — so a button no user can reach still passes the test. Use it only when you have confirmed the element is genuinely reachable and WebDriver’s hit test is wrong.

ActionChains

from selenium.webdriver.common.action_chains import ActionChains

with webdriver.Chrome(options=opts) as driver:
    driver.get("https://demo.playwright.dev/todomvc")
    driver.find_element(By.CLASS_NAME, "new-todo").send_keys("buy milk", Keys.ENTER)

    row = driver.find_element(By.CSS_SELECTOR, ".todo-list li")
    destroy = row.find_element(By.CSS_SELECTOR, ".destroy")

    print("before hover:", destroy.is_displayed())
    ActionChains(driver).move_to_element(row).perform()
    print("after hover: ", destroy.is_displayed())

    destroy.click()
    print("remaining:", len(driver.find_elements(By.CSS_SELECTOR, ".todo-list li")))
before hover: False
after hover:  True
remaining: 0

The delete button only exists visually on hover, so the earlier ElementNotInteractableException was correct behaviour, not a Selenium quirk. move_to_element is the fix.

The rest of the vocabulary:

actions = ActionChains(driver)
actions.double_click(element).perform()
actions.context_click(element).perform()                    # right-click
actions.click_and_hold(source).move_to_element(target).release().perform()
actions.drag_and_drop(source, target).perform()
actions.key_down(Keys.CONTROL).click(a).click(b).key_up(Keys.CONTROL).perform()
actions.move_to_element_with_offset(canvas, 40, 20).click().perform()
actions.scroll_to_element(element).perform()

Chained actions queue up and run on perform():

    ActionChains(driver) \
        .move_to_element(row) \
        .pause(0.2) \
        .double_click(row.find_element(By.CSS_SELECTOR, "label")) \
        .send_keys("buy oat milk") \
        .send_keys(Keys.ENTER) \
        .perform()

    print(driver.find_element(By.CSS_SELECTOR, ".todo-list li label").text)
buy oat milk

Double-click to edit, type, enter — one chain. pause() inside a chain is acceptable where a time.sleep between statements would not be: it is bounded, local, and usually needed for a CSS transition the DOM cannot report.

Note that HTML5 drag-and-drop frequently does not work with drag_and_drop, because the browser’s native DnD events are not fully driven by WebDriver. The common workaround is to dispatch the events via JavaScript, or to test the underlying reorder API directly.

File upload

UPLOAD = """data:text/html,
<input id="doc" type="file" multiple>
<p id="names"></p>
<script>
doc.onchange = () => names.textContent = [...doc.files].map(f => f.name).join(', ');
</script>"""

import os, tempfile

with webdriver.Chrome(options=opts) as driver:
    driver.get(UPLOAD)

    path = os.path.join(tempfile.mkdtemp(), "report.csv")
    with open(path, "w") as f:
        f.write("id,total\n1,25.50\n")

    driver.find_element(By.ID, "doc").send_keys(path)
    print(driver.find_element(By.ID, "names").text)
report.csv

Send the path to the input, do not click it. Clicking opens the operating system’s file dialog, which WebDriver cannot see or control — the test hangs until it times out.

Multiple files are newline-separated:

    driver.find_element(By.ID, "doc").send_keys(f"{path_a}\n{path_b}")
a.csv, b.csv

A hidden input needs revealing first — pages often hide the real input behind a styled button:

    driver.execute_script("document.getElementById('doc').style.display = 'block';")
    driver.find_element(By.ID, "doc").send_keys(path)

This is one of the few legitimate uses of execute_script: you are making a real element reachable, not faking an interaction.

Reading state

    el = driver.find_element(By.ID, "email")
    print("text:      ", driver.find_element(By.ID, "result").text)
    print("value:     ", el.get_attribute("value"))
    print("property:  ", el.get_property("value"))
    print("css:       ", el.value_of_css_property("display"))
    print("displayed: ", el.is_displayed())
    print("enabled:   ", el.is_enabled())
text:       ada@example.com | pro | true | sms | 
value:      ada@example.com
property:   ada@example.com
css:        inline-block
displayed:  True
enabled:    True

.text returns only visible text — a hidden element returns "" even when the DOM holds content. Read textContent when you need it regardless:

    print(repr(hidden.text))
    print(repr(hidden.get_attribute("textContent")))
''
'this is present but hidden'

That difference is behind a lot of confusing assertion failures.

Practice

1. Fill a form and assert the submitted values.
ada@example.com | pro | true | sms | deliver after 6pm

Then remove clear() before re-typing the email and re-run — the value appends rather than replaces. send_keys never overwrites.

2. Click a button covered by an overlay.
selenium.common.exceptions.ElementClickInterceptedException: Message: element click
intercepted: ... Other element would receive the click: <div class="cookie-banner">

The error names the blocker. Dismissing the banner is the fix; a JavaScript click is the shortcut that makes an unusable button look fine.

3. Hover a row to reveal its delete button.
before hover: False
after hover:  True

is_displayed() before and after proves the hover did something. Without move_to_element the click raises ElementNotInteractableException, which is correct — a user could not click it either.

4. Upload a file by clicking the input instead of sending a path.
selenium.common.exceptions.TimeoutException: Message: 
# ...after the OS file dialog opened and blocked the session

WebDriver has no access to native dialogs. send_keys(absolute_path) on the input is the only supported route.

Next: windows, frames and alerts — the contexts a locator cannot cross.

Frequently Asked Questions

Why does my click land on the wrong element?
Because something overlays the target — a cookie banner, a sticky header, a modal backdrop. Selenium clicks at the element's centre point, and the browser delivers that click to whatever is on top, which it reports as ElementClickInterceptedException.
Should I use JavaScript to click when a normal click fails?
Only as a last resort. `execute_script('arguments[0].click()')` bypasses visibility and overlay checks, so it will happily click a button no user can reach — turning a real bug into a passing test. Dismiss the overlay or scroll instead.
How do I upload a file in Selenium?
Call `send_keys` with an absolute path on the `<input type="file">` element itself. Never click the element to open the OS file dialog — WebDriver cannot control native dialogs, and the test will hang until it times out.
When do I need ActionChains?
For anything beyond a simple click or type: hover, drag and drop, right-click, double-click, click-and-hold, and modifier-key combinations. Everything else is better done with the element's own methods, which are simpler and more reliable.