WebElement State, Interactions, Forms, and Validation: Guided Hands-On Workflow
Operate a disposable loopback form fixture: inspect element state, fill fields, select options, toggle controls, submit valid and invalid data, and prove browser-visible and application-visible outcomes with evidence.
Learning objectives
- Create a disposable loopback-only form fixture and isolated Selenium 4.47.0 Python environment.
- Inspect text, attributes, DOM properties, displayed/enabled/selected state, and current values before interaction.
- Fill text fields, use a real HTML select, toggle checkbox/radio controls, and submit with native WebDriver commands.
- Compare invalid and valid submissions while verifying both browser-native validity and application-visible status.
- Capture returned capabilities, session identity, screenshots, state snapshots, and a JSON evidence report.
- Quit the browser and stop the local fixture cleanly without leaving profiles, servers, or synthetic state behind.
1. Lab contract and state map
This workflow uses one static HTML fixture served only from loopback. It contains synthetic fields for name, email, role, notification preference, plan selection, terms acceptance, and a status panel. JavaScript performs deterministic client-side validation and writes the accepted state into the page; there is no real account, network API, payment, or external identity system.
http://127.0.0.1:8765. The test asserts the hostname
before interaction. All data are synthetic, screenshots remain
inside the disposable lab directory, and the browser is always
closed in finally.
The following table organizes the key choices and evidence for Lab contract and state map. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| State store | Reads | Changes |
|---|---|---|
| WebDriver session | session ID, capabilities | created at driver start; destroyed by quit |
| DOM/control state | displayed/enabled/selected/value/validity | typing, clicking, selection |
| AUT page state | status text + submitted JSON in DOM | form submit handler |
| Evidence directory | screenshots + JSON | test runner writes files |
| Browser profile | temporary Selenium-managed profile | discarded when session ends |
2. Create the disposable project
The following example makes the Create the disposable project behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
mkdir selenium-element-lab
cd selenium-element-lab
python -m venv .venv
# Linux/macOS:
. .venv/bin/activate
# Windows PowerShell alternative:
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
mkdir site evidence
Verify the binding before creating browser state:
python -c "import selenium; print(selenium.__version__)"
python --version
3. Create the local form fixture
The following example makes the Create the local form fixture behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Profile Fixture</title>
<style>
body{font:16px system-ui;max-width:760px;margin:40px auto;padding:0 20px}
label{display:block;margin:14px 0 6px}.row{display:flex;gap:20px;align-items:center}
#status{margin-top:20px;padding:12px;border:1px solid #777}
[aria-invalid="true"]{outline:2px solid #b00}
</style>
</head>
<body data-build="ch05-v1">
<h1>Demo profile form</h1>
<form id="profile-form" novalidate>
<label for="name">Name</label>
<input id="name" name="name" required value="Seed User">
<label for="email">Email</label>
<input id="email" name="email" type="email" required value="seed@example.invalid">
<label for="role">Role</label>
<select id="role" name="role">
<option value="viewer">Viewer</option>
<option value="developer">Developer</option>
<option value="operator">Operator</option>
</select>
<div class="row">
<label><input id="newsletter" type="checkbox"> Newsletter</label>
<label><input name="plan" type="radio" value="basic" checked> Basic</label>
<label><input name="plan" type="radio" value="team"> Team</label>
</div>
<label><input id="terms" type="checkbox" required> Accept lab terms</label>
<button id="submit" type="submit">Save profile</button>
</form>
<div id="status" role="status" data-state="idle">No submission yet</div>
<pre id="result">{}</pre>
<script>
const form=document.querySelector('#profile-form');
const status=document.querySelector('#status');
form.addEventListener('submit', (event) => {
event.preventDefault();
for (const el of form.elements) {
if (el instanceof HTMLElement && 'checkValidity' in el) {
el.setAttribute('aria-invalid', String(!el.checkValidity()));
}
}
if (!form.checkValidity()) {
status.dataset.state='invalid';
status.textContent='Validation failed';
return;
}
const data={
name:form.name.value,
email:form.email.value,
role:form.role.value,
newsletter:form.querySelector('#newsletter').checked,
plan:form.querySelector('input[name="plan"]:checked').value,
terms:form.querySelector('#terms').checked
};
document.querySelector('#result').textContent=JSON.stringify(data,null,2);
status.dataset.state='saved';
status.textContent='Profile saved';
});
</script>
</body></html>
Save this as site/index.html, then serve only the
disposable directory:
python -m http.server 8765 --bind 127.0.0.1 --directory site
Keep that terminal open. A browser request to
http://127.0.0.1:8765/ should show “Demo profile form.”
4. Read state before changing it
The following example makes the Read state before changing it behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from pathlib import Path
import json, selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
BASE_URL = "http://127.0.0.1:8765/"
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)
driver = webdriver.Chrome()
try:
driver.get(BASE_URL)
assert driver.current_url.startswith("http://127.0.0.1:8765/")
name = driver.find_element(By.ID, "name")
terms = driver.find_element(By.ID, "terms")
submit = driver.find_element(By.ID, "submit")
before = {
"selenium": selenium.__version__,
"session_id": driver.session_id,
"browser": driver.capabilities.get("browserName"),
"browser_version": driver.capabilities.get("browserVersion"),
"title": driver.title,
"build": driver.find_element(By.TAG_NAME, "body").get_dom_attribute("data-build"),
"name_attribute": name.get_dom_attribute("value"),
"name_property": name.get_property("value"),
"terms_selected": terms.is_selected(),
"submit_displayed": submit.is_displayed(),
"submit_enabled": submit.is_enabled(),
}
print(json.dumps(before, indent=2))
finally:
driver.quit()
Expected observations: the declared and runtime name value are both
Seed User initially; terms are not selected; submit is
displayed and enabled; the page state is still idle.
5. Run an invalid interaction path and prove why it failed
Now deliberately create invalid browser state. Do not assert only that clicking the button happened; inspect HTML validity and the application status written by the fixture.
from selenium import webdriver
from selenium.webdriver.common.by import By
BASE_URL = "http://127.0.0.1:8765/"
driver = webdriver.Chrome()
try:
driver.get(BASE_URL)
email = driver.find_element(By.ID, "email")
terms = driver.find_element(By.ID, "terms")
submit = driver.find_element(By.ID, "submit")
email.clear()
email.send_keys("not-an-email")
assert email.get_property("value") == "not-an-email"
assert email.get_property("validity")["valid"] is False
assert terms.is_selected() is False
submit.click()
status = driver.find_element(By.ID, "status")
assert status.get_dom_attribute("data-state") == "invalid"
assert status.text == "Validation failed"
finally:
driver.quit()
The click can complete successfully while the application rejects the form. That is the central chapter distinction: WebDriver interaction success and business/application success are separate assertions.
6. Run the valid workflow with native controls
The following example makes the Run the valid workflow with native controls behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from pathlib import Path
import json, selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
BASE_URL = "http://127.0.0.1:8765/"
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)
driver = webdriver.Chrome()
try:
driver.get(BASE_URL)
assert driver.current_url.startswith("http://127.0.0.1:8765/")
name = driver.find_element(By.ID, "name")
email = driver.find_element(By.ID, "email")
newsletter = driver.find_element(By.ID, "newsletter")
terms = driver.find_element(By.ID, "terms")
name.clear(); name.send_keys("Ava Example")
email.clear(); email.send_keys("ava@example.invalid")
Select(driver.find_element(By.ID, "role")).select_by_value("developer")
if not newsletter.is_selected(): newsletter.click()
driver.find_element(By.CSS_SELECTOR, 'input[name="plan"][value="team"]').click()
if not terms.is_selected(): terms.click()
predicted = {
"name": name.get_property("value"),
"email_valid": email.get_property("validity")["valid"],
"newsletter": newsletter.is_selected(),
"terms": terms.is_selected(),
}
print("pre-submit", predicted)
driver.find_element(By.ID, "submit").click()
status = driver.find_element(By.ID, "status")
result = json.loads(driver.find_element(By.ID, "result").text)
assert status.get_dom_attribute("data-state") == "saved"
assert status.text == "Profile saved"
assert result == {
"name":"Ava Example", "email":"ava@example.invalid",
"role":"developer", "newsletter":True, "plan":"team", "terms":True
}
driver.save_screenshot(str(EVIDENCE / "valid-submit.png"))
report = {
"selenium": selenium.__version__, "session_id": driver.session_id,
"capabilities": {k: driver.capabilities.get(k) for k in ("browserName","browserVersion","platformName")},
"url": driver.current_url, "status": status.text, "result": result
}
(EVIDENCE / "valid-submit.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
finally:
driver.quit()
The test verifies control state before submission and application state after submission. The screenshot and JSON report are evidence, not the assertion itself.
7. Challenge: choose the right control, not the shortest code
Add a “Contact method” radio group with values
email and none. Requirement: if
none is selected, email may be blank; otherwise a valid
email is required. Before coding, decide which checks belong to
is_selected(), which belong to browser validity
properties, and which belong to the application status/result. Do
not solve the requirement with JavaScript injection.
8. Verification and cleanup
- Binding evidence shows Selenium 4.47.0 and Python 3.10+.
- Browser/session capabilities and session ID are recorded for at least one run.
-
Invalid input produces application state
invalideven though the submit click itself executes. -
Valid input produces
savedand the exact synthetic result JSON. -
A screenshot and JSON evidence file exist under the disposable
evidence/directory. -
Every driver is closed with
quit()and the loopback HTTP server is stopped when finished.
The following example makes the Verification and cleanup behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# Stop the http.server with Ctrl+C in its terminal, then:
deactivate 2>/dev/null || true
cd ..
rm -rf selenium-element-lab
Knowledge check
Why does the invalid form test still call submit.click()?
Because the test wants to prove the browser can perform the user action and the application then rejects invalid state; click success and application acceptance are separate facts.
Why inspect email validity before submission?
It gives causal evidence about browser control state before the application handler runs, making a later validation failure easier to interpret.
Why use Select for role?
The fixture uses a real HTML select element, so Select expresses the control semantics directly and portably.
What should be cleaned up after the lab?
The WebDriver session/browser, loopback HTTP server, disposable virtual environment, fixture files, and evidence directory if they are no longer needed.
Why is the URL guard important even in a harmless course example?
It prevents the automation code from accidentally targeting a non-lab environment if the base URL is changed or supplied incorrectly.
Official references and version notes
- Selenium 4.47 release — current pinned Selenium release baseline for this chapter.
- Interacting with web elements — click, send keys, clear, interactability checks, and click-interception semantics.
- Information about web elements — displayed, enabled, selected, text, CSS, and attribute/property inspection.
-
Python WebElement API 4.47.0
— current
get_dom_attribute,get_property,get_attribute, interaction, and state-query APIs. -
Working with select list elements
— current support-library semantics for real HTML
<select>controls.
Version-sensitive behavior was rechecked against Selenium primary documentation on 2026-08-28. Mandatory examples pin Selenium Python 4.47.0 on Python 3.10+, use a supported locally installed Chromium-family browser, ordinary Selenium Manager resolution, and loopback-only fixtures. Grid, browser clouds, enterprise identity, and WebDriver BiDi are not required in this chapter.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.