Chapter 01Lesson 05~180 minutes

Checkpoint Lab — Browser Automation and Test Engineering Foundations

Create a tiny browser-automation test charter, implement one reliable happy path and one negative path against a disposable local app, collect an evidence packet, and document which checks belong below the browser.

CheckpointTest charterPositive/negativeEvidence packetReproducibility

Learning objectives

  • Translate product risk into a two-test browser charter.
  • Predict browser/session/DOM/AUT/evidence changes before executing tests.
  • Use a fresh WebDriver session for each independent test.
  • Capture only safe correlated capability, screenshot, and DOM evidence.
  • Run a standard-library test runner with reliable failure exit status.
  • Produce cleanup and reproducibility notes that another engineer can follow.

1. Checkpoint acceptance contract

You will prove two user-visible behaviors for a local order fixture: a valid quantity is accepted and an invalid quantity is rejected. You will not use Selenium to load-test the fixture, validate every numeric rule permutation, or create external accounts. The checkpoint is successful only when both tests are independent, use fresh browser sessions, assert visible intended state, write evidence into separate directories, and quit cleanly.

Risk Chosen layer Why
Visible form accepts valid quantity Selenium Browser input + click + visible status are the behavior.
Visible form rejects invalid quantity Selenium User-visible validation mapping is the behavior.
All numeric boundary/rounding rules Unit/service (documented, not implemented here) Many data combinations are faster and clearer below the UI.
Server under 500 concurrent users Dedicated load tool Selenium is not the load generator.

2. Setup and preflight

The following example makes the Setup and preflight behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

mkdir selenium-ch01-checkpoint
cd selenium-ch01-checkpoint
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

Assumptions: Python 3.10+, Selenium Python 4.47.0, one installed supported local browser, Selenium Manager available through the binding, loopback networking, and no Selenium Grid. Before execution, record the browser family you expect; after session creation, trust the returned capabilities for actual browser/version/platform evidence.

3. Create the exact disposable application

Save as app/index.html:

The following example makes the Create the exact disposable application 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>Order Charter Lab</title></head>
<body>
<main>
  <h1>Order Charter Lab</h1>
  <label>Item <input data-testid="item" value="widget" readonly></label>
  <label>Quantity <input data-testid="quantity" type="number" min="1" max="9" value="1"></label>
  <button data-testid="submit" type="button">Create order</button>
  <p data-testid="result" role="status">No order submitted</p>
</main>
<script>
const quantity = document.querySelector('[data-testid="quantity"]');
const result = document.querySelector('[data-testid="result"]');
document.querySelector('[data-testid="submit"]').addEventListener('click', () => {
  const q = Number(quantity.value);
  setTimeout(() => {
    if (!Number.isInteger(q) || q < 1 || q > 9) {
      result.textContent = 'Quantity must be between 1 and 9';
      result.dataset.state = 'invalid';
      return;
    }
    result.textContent = `Order accepted: widget x ${q}`;
    result.dataset.state = 'accepted';
  }, 200);
});
</script>
</body>
</html>

The application is intentionally tiny. It has one item, one quantity, one button, and one visible status. The delayed update creates a real synchronization boundary but no external dependency.

4. Start the loopback server

The following example makes the Start the loopback server 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 8770 --bind 127.0.0.1 --directory app

Keep this terminal open. Verify the bind address is loopback. If another process already uses port 8770, choose another local port and set the same value through SELENIUM_LAB_URL; do not change the target to a public environment.

5. Write predictions before execution

Record these predictions in your notes before creating the test file:

  1. Session: each test method creates a different session ID and fresh browser state.
  2. DOM/AUT: happy path changes the quantity to 3 and eventually sets visible result text/state to accepted; negative path changes it to 0 and eventually sets visible result text/state to invalid.
  3. Evidence: each test gets its own directory containing safe capability JSON plus PNG/HTML evidence.
  4. Cleanup: addCleanup(driver.quit) releases each session even when an assertion raises.

6. Implement the two-test charter

Save as test_order_charter.py:

The following example makes the Implement the two-test charter behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

import json
import os
import unittest
from pathlib import Path

import selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

BASE_URL = os.environ.get("SELENIUM_LAB_URL", "http://127.0.0.1:8770/")
EVIDENCE_ROOT = Path("evidence")


def require_loopback(url: str) -> None:
    allowed = ("http://127.0.0.1:", "http://localhost:")
    if not url.startswith(allowed):
        raise RuntimeError(f"Safety guard refused target: {url}")


class OrderCharterTests(unittest.TestCase):
    def setUp(self):
        require_loopback(BASE_URL)
        self.assertEqual(selenium.__version__, "4.47.0")
        self.driver = webdriver.Chrome()
        self.addCleanup(self.driver.quit)
        self.case_evidence = EVIDENCE_ROOT / self._testMethodName
        self.case_evidence.mkdir(parents=True, exist_ok=True)

        caps = self.driver.capabilities
        safe_caps = {
            "selenium": selenium.__version__,
            "session_id": self.driver.session_id,
            "browserName": caps.get("browserName"),
            "browserVersion": caps.get("browserVersion"),
            "platformName": caps.get("platformName"),
        }
        (self.case_evidence / "capabilities.json").write_text(
            json.dumps(safe_caps, indent=2), encoding="utf-8"
        )

    def open_app(self):
        self.driver.get(BASE_URL)
        self.assertEqual(self.driver.title, "Order Charter Lab")

    def wait_result(self, expected: str):
        result = self.driver.find_element(By.CSS_SELECTOR, '[data-testid="result"]')
        WebDriverWait(self.driver, 3).until(lambda d: result.text == expected)
        return result

    def save_evidence(self, name: str):
        self.driver.save_screenshot(str(self.case_evidence / f"{name}.png"))
        (self.case_evidence / f"{name}.html").write_text(
            self.driver.page_source, encoding="utf-8"
        )

    def test_happy_path_accepts_valid_quantity(self):
        self.open_app()
        quantity = self.driver.find_element(By.CSS_SELECTOR, '[data-testid="quantity"]')
        quantity.clear()
        quantity.send_keys("3")
        self.driver.find_element(By.CSS_SELECTOR, '[data-testid="submit"]').click()
        result = self.wait_result("Order accepted: widget x 3")
        self.assertEqual(result.get_attribute("data-state"), "accepted")
        self.save_evidence("happy")

    def test_negative_path_rejects_invalid_quantity(self):
        self.open_app()
        quantity = self.driver.find_element(By.CSS_SELECTOR, '[data-testid="quantity"]')
        quantity.clear()
        # send_keys bypasses browser number-input spinner constraints but still exercises the DOM value.
        quantity.send_keys("0")
        self.driver.find_element(By.CSS_SELECTOR, '[data-testid="submit"]').click()
        result = self.wait_result("Quantity must be between 1 and 9")
        self.assertEqual(result.get_attribute("data-state"), "invalid")
        self.save_evidence("negative")


if __name__ == "__main__":
    unittest.main(verbosity=2)

The standard-library unittest runner supplies test discovery, per-test setup, assertions, reporting, and a non-zero process exit status when a test fails. Selenium remains the browser-control layer rather than being mislabeled as the test runner.

7. Execute and preserve output

The following example makes the Execute and preserve output behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

python -m unittest -v test_order_charter.py
python - <<'PY'
from pathlib import Path
for p in sorted(Path('evidence').rglob('*')):
    if p.is_file():
        print(p)
PY

Expected runner output contains two ok results and an overall OK. Expected evidence includes separate directories for test_happy_path_accepts_valid_quantity and test_negative_path_rejects_invalid_quantity, each with capabilities.json and its corresponding screenshot/page source.

8. Verify the predictions independently

  • Compare the two session_id values in capability files; they must differ.
  • Open happy.png and verify Order accepted: widget x 3.
  • Open negative.png and verify Quantity must be between 1 and 9.
  • Confirm each HTML snapshot contains the matching data-state value.
  • After the runner exits, verify no intentionally retained browser session remains.

9. Build the checkpoint evidence packet

Your evidence packet should contain:

  • the exact command used to create the virtual environment and install selenium==4.47.0;
  • Python and Selenium versions;
  • safe returned capability fields (browser name/version/platform) and session IDs;
  • the two test results and process exit status;
  • happy/negative screenshots and page-source snapshots from synthetic data only;
  • the four predictions above and whether each was confirmed;
  • a note that Grid/BiDi/CI were not required in Chapter 01;
  • a cleanup record.

10. Prove failure semantics once

In a temporary copy of the test file, change the happy-path expected text to an impossible value. Run only that test. Confirm the runner exits non-zero, the wait/exception is visible, and cleanup still closes the browser. Restore the original file. Do not add retries or extend the timeout to conceal the incorrect expectation.

11. What belongs in Selenium versus lower layers

Document at least three checks you would not add as more browser tests:

  1. dozens of quantity boundary permutations → unit/service tests;
  2. raw server response/status contract → HTTP/API test;
  3. throughput under concurrency → JMeter or another dedicated load tool.

Keep the two browser scenarios because they verify user-visible browser interaction and state mapping. This classification is part of the checkpoint, not optional commentary.

12. Production operating model gained in Chapter 01

You now have a foundation that scales beyond the tiny fixture: explicit test intent, clear ownership boundaries, version/session evidence, loopback/environment guards, fresh browser isolation, condition-based readiness, meaningful assertions, first-failure diagnostics, and deterministic teardown. These are the controls that make later locators, Grid, BiDi, parallelism, and CI useful instead of merely more complex.

13. Cleanup and rollback

The following example makes the Cleanup and rollback behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Stop the loopback server with Ctrl+C first.
cd ..
# Preserve the checkpoint evidence elsewhere only if required.
rm -rf selenium-ch01-checkpoint

Windows PowerShell equivalent for the final deletion is Remove-Item -Recurse -Force .\selenium-ch01-checkpoint after stopping the server. Delete only the disposable lab directory you created; do not clear global browser profiles, caches, or system driver directories as part of this course lab.

Knowledge check

Why does each checkpoint test create a new WebDriver instance?

Why are capability files stored per test rather than once globally?

What failure does the loopback URL guard prevent?

Why should quantity-boundary permutations stay below Selenium?

What proves that teardown is part of correctness?

What does Chapter 02 add next?

Next chapter

Chapter 02 — Selenium Ecosystem, Installation, and First WebDriver Session

The next chapter zooms into installation and session creation: language bindings, Selenium Manager, browser/driver discovery, local service lifecycle, options, compatibility evidence, and first-session troubleshooting.

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.