Chapter 05Lesson 02~190 minutes

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.

Loopback labForm workflowSelectValidationEvidence

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.

Safety boundary: use only 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 invalid even though the submit click itself executes.
  • Valid input produces saved and 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()?

Why inspect email validity before submission?

Why use Select for role?

What should be cleaned up after the lab?

Why is the URL guard important even in a harmless course example?

Next lesson

Configuration, Design Patterns, and Trade-Offs

Choose where native interaction, application-state assertions, API-assisted setup, and form semantics belong in a maintainable test architecture.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.