Chapter 01Lesson 02~120 minutes

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.

Loopback labPythonAssertionEvidenceTeardown

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.

Prediction before action: before the click, the status text is 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.

Do not “fix” the intentional failure by increasing the timeout to a giant value or adding blanket retries. The expected value is wrong; more waiting cannot make it correct.

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.png corresponds 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-lab directory when evidence is no longer needed.

Knowledge check

Why does this lesson include both an HTTP check and a Selenium check?

What makes the explicit wait more meaningful than sleep(5)?

Which operation creates the WebDriver session?

Why is the loopback guard part of test engineering rather than convenience?

What should happen to the session even when an assertion fails?

Next lesson

Choose where browser realism is worth its cost

Lesson 3 turns this workflow into design decisions: which checks belong in Selenium, which should stay below the UI, and how to balance realism, maintainability, runtime, and diagnostic quality.

Official references and version notes

Version and compatibility note

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.

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