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("[email protected]")
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)
[email protected] | pro | true | sms | deliver after 6pm
send_keys appends — it does not replace. To overwrite an existing value:
email.clear()
email.send_keys("[email protected]")
print(email.get_attribute("value"))
[email protected]
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("[email protected]")
print(email.get_attribute("value"))
[email protected]
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: [email protected] | pro | true | sms |
value: [email protected]
property: [email protected]
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.
[email protected] | 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.