Browser Automation and Test Engineering Foundations: Guided Hands-On Workflow
Build one disposable local application and trace a complete Selenium workflow from environment preflight through navigation, interaction, assertion, evidence capture, and deterministic teardown.
Learning objectives
- Create a loopback-only AUT that cannot accidentally mutate a production service.
- Run both a lower-layer HTTP check and a real browser check against the same fixture.
- Inspect session/capability/URL state before and after navigation.
- Use stable test-owned selectors and one bounded explicit wait.
- Capture a screenshot as correlated evidence rather than as decoration.
- Quit the WebDriver session even when an assertion or command fails.
1. Workflow contract
The smallest useful browser test still needs more than “open page,
click button.” It needs a known target, known test data, an explicit
session lifecycle, an observable assertion, failure evidence, and
cleanup. The lab below deliberately uses a static site served from
127.0.0.1 so the network boundary is obvious and no
account or external service is involved.
Waiting. After the click, the
browser-side script schedules a 250 ms state change. The WebDriver
test must wait for the intended status, not for an arbitrary amount
of wall-clock time.
2. Preflight and isolated environment
The following example makes the Preflight and isolated environment behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
mkdir selenium-foundations-lab
cd selenium-foundations-lab
python -m venv .venv
# Linux/macOS
. .venv/bin/activate
# Windows PowerShell equivalent: .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
python -c "import sys,selenium; print(sys.version); print('selenium', selenium.__version__)"
mkdir app evidence
Use Python 3.10+ and an installed supported browser. The required path uses local Chrome in examples because it keeps the code compact; Firefox or Edge are legitimate alternatives when you adapt the constructor and record their returned capabilities. No Selenium Server/Grid is required for local execution.
3. Create the disposable AUT
Save this as app/index.html. The
data-testid attributes are deliberately owned by the
test contract; the lesson does not depend on generated classes or
fragile DOM traversal.
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Automation Lab</title></head>
<body>
<main>
<h1>Automation Lab</h1>
<label>Display name <input data-testid="name" autocomplete="off"></label>
<button data-testid="save" type="button">Save</button>
<p data-testid="status" aria-live="polite">Waiting</p>
</main>
<script>
const name = document.querySelector('[data-testid="name"]');
const status = document.querySelector('[data-testid="status"]');
document.querySelector('[data-testid="save"]').addEventListener('click', () => {
const value = name.value.trim();
setTimeout(() => {
status.textContent = value ? `Saved: ${value}` : 'Name is required';
}, 250);
});
</script>
</body>
</html>
The button does not send a real request or update a database. It only changes visible DOM text after a short delay, which gives us a controlled synchronization boundary without involving an external system.
4. Serve only on loopback
In terminal A, from the lab root:
The following example makes the Serve only on loopback behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python -m http.server 8765 --bind 127.0.0.1 --directory app
Expected console output reports a server listening on
127.0.0.1:8765. Open the page manually once if useful,
but do not change the fixture to a public bind address. Loopback is
the safety guard for this exercise.
5. First compare what an HTTP check can prove
A browser is not necessary to prove that the server responds with the fixture HTML. Python’s standard library can do that faster and with fewer failure layers. The browser test is justified because we also want to prove browser-side interaction and rendered state.
| Check | Proves | Does not prove |
|---|---|---|
urlopen() |
Server is reachable, returns HTTP 200, expected HTML marker exists. | Browser rendering, DOM interaction, JavaScript event behavior. |
| Selenium | A real browser can load, locate controls, send keys/click, and expose the expected visible state. | Every backend/business invariant or performance under load. |
6. Execute the browser workflow
Save this as workflow.py. The code uses one primary
Python binding, a hard loopback guard, an explicit wait tied to
business-visible state, a screenshot, and
finally teardown.
from pathlib import Path
from urllib.request import urlopen
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
BASE_URL = "http://127.0.0.1:8765/"
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)
def local_only(url: str) -> None:
if not (url.startswith("http://127.0.0.1:") or url.startswith("http://localhost:")):
raise RuntimeError(f"Refusing non-loopback target: {url}")
def http_check() -> None:
local_only(BASE_URL)
with urlopen(BASE_URL, timeout=3) as response:
body = response.read().decode("utf-8")
assert response.status == 200
assert "Automation Lab" in body
def browser_check() -> None:
local_only(BASE_URL)
driver = webdriver.Chrome()
try:
print("session_id=", driver.session_id)
print("capabilities=", {
"browserName": driver.capabilities.get("browserName"),
"browserVersion": driver.capabilities.get("browserVersion"),
"platformName": driver.capabilities.get("platformName"),
})
# Observe the fresh session before navigation.
print("before_url=", driver.current_url)
driver.get(BASE_URL)
assert driver.title == "Automation Lab"
name = driver.find_element(By.CSS_SELECTOR, '[data-testid="name"]')
save = driver.find_element(By.CSS_SELECTOR, '[data-testid="save"]')
status = driver.find_element(By.CSS_SELECTOR, '[data-testid="status"]')
name.send_keys("learner-example")
save.click()
WebDriverWait(driver, 3).until(
lambda d: status.text == "Saved: learner-example"
)
assert status.text == "Saved: learner-example"
assert driver.current_url == BASE_URL
driver.save_screenshot(str(EVIDENCE / "workflow.png"))
finally:
driver.quit()
if __name__ == "__main__":
http_check()
browser_check()
print("PASS: HTTP contract and browser workflow both verified")
Run it from terminal B:
The following example makes the Execute the browser workflow behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python workflow.py
ls evidence 2>/dev/null || dir evidence
Expected observations are a successful HTTP check, a non-empty
WebDriver session ID, returned browser/platform capabilities, the
initial blank-session URL, a final PASS line, and
evidence/workflow.png. The screenshot should show
Saved: learner-example.
7. State changes, step by step
The following table organizes the key choices and evidence for State changes, step by step. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| Action | State read/changed | Evidence |
|---|---|---|
webdriver.Chrome() |
Creates driver service/browser process and a WebDriver session/profile. | Session ID + returned capabilities. |
driver.get(BASE_URL) |
Changes current browsing context URL and loads AUT resources. | URL/title/DOM. |
find_element |
Reads current DOM and creates element references. | Located elements, or a locator exception. |
send_keys/click |
Changes form value and triggers browser/AUT JavaScript. | Input value, later status text. |
WebDriverWait |
Polls intended state until true or timeout. | Success or bounded timeout with preserved state. |
save_screenshot |
Writes evidence into the test workspace. | PNG artifact. |
quit |
Ends the full WebDriver session/browser. | No intentionally retained live session. |
8. Why the wait is a condition, not a sleep
The fixture changes state after 250 ms. A fixed
sleep(0.25) assumes the scheduler/browser always
completes on time; sleep(5) hides the race by wasting
time. The explicit wait asks the question we actually care about:
“has the visible status become the intended value?” Selenium’s
current waiting documentation identifies application/test race
conditions as a primary source of flakiness and recommends
condition-based synchronization. The course will develop this deeply
in Chapter 06.
9. Why these selectors are intentionally boring
[data-testid="save"] is compact, stable, and readable
because the fixture explicitly owns that attribute for automation. A
selector such as
body > main:nth-child(1) > button:nth-child(3)
encodes incidental document structure. Both might work today; only
one expresses durable identity. Locator engineering gets its full
treatment in Chapter 04.
10. One controlled failure
Change the final expected status in a disposable copy of
workflow.py to Saved: wrong-name. Predict
the result first: the browser action will succeed, but the condition
will time out because the AUT never reaches that state. Run the copy
once, preserve the exception, then restore the correct expectation.
This separates interaction success from
asserted business-visible state.
11. Small challenge
Add a second assertion that the input still contains
learner-example after the save. Decide whether that
assertion adds useful intent or merely duplicates implementation
detail. Keep it only if your imagined product requirement says the
value should remain editable/visible after saving. The exercise is
to choose an assertion from test intent, not from whatever state
happens to be easy to read.
12. Verification and cleanup
- Verify the server is bound only to
127.0.0.1. -
Verify
workflow.pngcorresponds to the current run and contains no sensitive data. - Verify the browser closes after success and after the intentional failure.
- Stop terminal A with Ctrl+C.
-
Delete the disposable
selenium-foundations-labdirectory when evidence is no longer needed.
Knowledge check
Why does this lesson include both an HTTP check and a Selenium check?
The HTTP check proves a lower-layer reachability/content contract cheaply; Selenium is reserved for the browser-specific interaction and rendered state that the HTTP client cannot prove.
What makes the explicit wait more meaningful than sleep(5)?
It polls the exact intended state and returns as soon as that state is true, while a fixed sleep guesses at timing and can be either too short or wastefully long.
Which operation creates the WebDriver session?
webdriver.Chrome() creates the local session in
this example. Selenium Manager may participate in
locating/managing the driver when one was not supplied.
Why is the loopback guard part of test engineering rather than convenience?
It prevents this educational script from being casually pointed at an external or production-like target, making the safety boundary executable.
What should happen to the session even when an assertion fails?
The finally block must call
driver.quit() so the session/browser is
deterministically released.
Official references and version notes
- Selenium 4.47 release notes — release baseline used by this chapter.
- Selenium downloads — current binding and Selenium Server/Grid version status.
- WebDriver getting started — current WebDriver/driver mental model.
- Selenium Manager — automated browser/driver management behavior.
- Waiting strategies — race conditions, implicit waits, explicit waits, and the warning about mixing them.
- Avoid sharing state and Fresh browser per test — isolation guidance.
- Troubleshooting assistance — synchronization and cross-browser diagnostic guidance.
- Selenium Python 4.47.0 package metadata — Python 3.10+ requirement and supported browser families.
Version-sensitive statements were rechecked against current Selenium primary documentation on 2026-08-27. Mandatory examples pin the Python Selenium binding to 4.47.0, require Python 3.10+, use an installed supported local browser with Selenium Manager as the default driver-management path, and do not require Selenium Grid, a paid browser cloud, enterprise identity, or a production website. Record the browser and driver versions returned by the actual session because those remain environment-specific.
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.