Chapter 06Lesson 05~250 minutes

Checkpoint Lab — Synchronization: Implicit, Explicit, Fluent, and Custom Waits

Stabilize a deliberately flaky SPA-like local test, measure before/after behavior, implement a custom wait tied to an observable state transition, preserve first-failure evidence, and document cleanup and CI implications.

CheckpointFlake baselineCustom conditionEvidence packetCleanup

Learning objectives

  • Run a deterministic set of SPA-like delays and record the pass/fail pattern of a deliberately brittle fixed-sleep baseline.
  • Replace the brittle baseline with bounded waits tied to observable DOM/application transitions without increasing fixed sleeps.
  • Implement one custom condition that validates both application phase and run identity.
  • Predict at least two state transitions before execution and verify the predictions independently afterward.
  • Produce an evidence packet containing versions, session IDs, capabilities, timing results, screenshots, DOM state, and diagnostics.
  • Document verification, cleanup, reproducibility assumptions, and the bridge to Chapter 07 Actions API.

1. Checkpoint scenario and safety boundary

You will stabilize a small SPA-like fixture whose result appears after deterministic but varying delays. The brittle baseline uses one fixed sleep so it passes some cases and fails others. The stable run uses bounded explicit/custom conditions with implicit wait kept at zero. You will record the before/after pass pattern, verify predicted state transitions, and keep evidence from the first baseline failures.

Local-only requirement: use only http://127.0.0.1:8765, synthetic run IDs, and a disposable browser session. Do not point this exercise at a production application, real account, public Grid, or sensitive data. The lab does not require paid services, Docker, Grid, BiDi, or enterprise infrastructure.
  • Selenium Python is pinned to 4.47.0; Python 3.10+ is required.
  • A supported local Chromium-family browser is installed; record the actual browser version returned by capabilities.
  • Selenium Manager may resolve the local driver normally; record browser/session provenance rather than assuming versions.
  • The test uses no implicit wait while running explicit/custom conditions.
  • The fixed sleep exists only in the deliberately flaky baseline and is removed from the stabilized path.
  • The evidence directory contains only synthetic local test artifacts and is deleted during cleanup after review.

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-ch06-checkpoint
cd selenium-ch06-checkpoint
python -m venv .venv
# Linux/macOS:
. .venv/bin/activate
# Windows PowerShell:
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
mkdir site evidence

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.

python -c "import selenium; print('selenium', selenium.__version__)"
python --version

Confirm the local port is free and no production browser profile is being reused. The WebDriver session created below owns its temporary automation profile and is closed in finally.

3. Create the SPA-like timing fixture

The following example makes the Create the SPA-like timing 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>Chapter 06 Checkpoint</title>
<style>body{font:16px system-ui;max-width:760px;margin:40px auto;padding:0 20px}#status,#result{padding:10px;margin-top:12px;border:1px solid #777}</style></head>
<body data-build="ch06-checkpoint-v1">
<h1>Async build result</h1>
<button id="run">Run</button>
<div id="status" data-phase="idle" data-run-id="">Idle</div>
<div id="mount"></div>
<script>
const p = new URLSearchParams(location.search);
const delay = Number(p.get('delay') || '700');
const runId = p.get('run') || 'R0';
const status = document.querySelector('#status');
const mount = document.querySelector('#mount');
document.querySelector('#run').addEventListener('click', () => {
  status.dataset.phase='working'; status.dataset.runId=runId; status.textContent=`Working ${runId}`;
  mount.replaceChildren();
  setTimeout(() => {
    const result=document.createElement('div'); result.id='result'; result.dataset.runId=runId;
    result.textContent=`Complete ${runId} after ${delay} ms`; mount.append(result);
    status.dataset.phase='complete'; status.textContent=`Complete ${runId}`;
  }, delay);
});
</script></body></html>

Save as site/index.html and start the fixture in another terminal:

python -m http.server 8765 --bind 127.0.0.1 --directory site

4. Write predictions before execution

The following example makes the Write predictions before execution behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

Prediction 1: immediately after Run, status phase changes idle -> working and #result is absent.
Prediction 2: with a fixed 0.5 s sleep, delay cases below 500 ms should usually pass while cases above 500 ms should fail because #result is still absent.
Prediction 3: the stabilized wait should pass every configured delay <= 1.4 s within a 2.2 s deadline without increasing any fixed sleep.
Prediction 4: each accepted result must carry the current run ID so a stale result from another run cannot satisfy the wait.

These are falsifiable. If actual behavior differs, preserve the evidence and diagnose it instead of changing the expected narrative after the run.

5. Record the deliberately flaky baseline

The following baseline is intentionally wrong. It exists so you can measure a pass/fail pattern before repair.

import time
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException

BASE = "http://127.0.0.1:8765/"
DELAYS = [120, 700, 250, 1100, 400, 1400]
E = Path("evidence"); E.mkdir(exist_ok=True)
driver = webdriver.Chrome()
results = []
try:
    assert driver.timeouts.implicit_wait == 0
    for i, delay in enumerate(DELAYS, 1):
        run_id = f"B{i}"
        driver.get(f"{BASE}?delay={delay}&run={run_id}")
        driver.find_element(By.ID, "run").click()
        time.sleep(0.5)  # INTENTIONALLY BRITTLE BASELINE ONLY
        try:
            result = driver.find_element(By.ID, "result")
            ok = result.get_dom_attribute("data-run-id") == run_id
            results.append({"run":run_id,"delay_ms":delay,"passed":ok})
        except NoSuchElementException as exc:
            results.append({"run":run_id,"delay_ms":delay,"passed":False,"error":type(exc).__name__})
            driver.save_screenshot(str(E / f"baseline-{run_id}-failure.png"))
finally:
    driver.quit()

print(results)

Expected pattern: shorter delays pass and longer delays fail. Exact scheduling can vary slightly by machine; what matters is that pass/fail correlates with the guessed 0.5-second sleep rather than with an application-ready condition. Save the printed result list as baseline evidence.

6. Implement the application-specific wait

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

from selenium.webdriver.common.by import By

class RunComplete:
    def __init__(self, run_id):
        self.run_id = run_id

    def __call__(self, driver):
        status = driver.find_element(By.ID, "status")
        phase = status.get_dom_attribute("data-phase")
        current = status.get_dom_attribute("data-run-id")
        if current != self.run_id or phase != "complete":
            return False
        result = driver.find_element(By.ID, "result")
        return result if result.get_dom_attribute("data-run-id") == self.run_id else False

The condition is intentionally stronger than presence. It binds readiness to the current run identity and the application’s terminal phase, then returns the current result element.

7. Run the stabilized suite and collect evidence

The following example makes the Run the stabilized suite and collect evidence 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
from time import monotonic
import json, selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException

BASE = "http://127.0.0.1:8765/"
DELAYS = [120, 700, 250, 1100, 400, 1400]
E = Path("evidence"); E.mkdir(exist_ok=True)
driver = webdriver.Chrome()
report = {"selenium": selenium.__version__, "stable_runs": []}
try:
    assert driver.timeouts.implicit_wait == 0
    report.update({
        "session_id": driver.session_id,
        "browser": driver.capabilities.get("browserName"),
        "browser_version": driver.capabilities.get("browserVersion"),
        "platform": driver.capabilities.get("platformName"),
        "implicit_wait": driver.timeouts.implicit_wait,
    })

    for i, delay in enumerate(DELAYS, 1):
        run_id = f"S{i}"
        driver.get(f"{BASE}?delay={delay}&run={run_id}")
        assert driver.find_element(By.TAG_NAME, "body").get_dom_attribute("data-build") == "ch06-checkpoint-v1"
        assert driver.find_element(By.ID, "status").get_dom_attribute("data-phase") == "idle"

        driver.find_element(By.ID, "run").click()
        status = driver.find_element(By.ID, "status")
        assert status.get_dom_attribute("data-phase") == "working"
        assert status.get_dom_attribute("data-run-id") == run_id

        t0 = monotonic()
        try:
            result = WebDriverWait(driver, 2.2, poll_frequency=0.2).until(RunComplete(run_id))
        except TimeoutException:
            driver.save_screenshot(str(E / f"stable-{run_id}-timeout.png"))
            (E / f"stable-{run_id}-source.html").write_text(driver.page_source, encoding="utf-8")
            raise
        elapsed = monotonic() - t0

        assert result.get_dom_attribute("data-run-id") == run_id
        assert driver.find_element(By.ID, "status").get_dom_attribute("data-phase") == "complete"
        report["stable_runs"].append({
            "run": run_id,
            "delay_ms": delay,
            "passed": True,
            "elapsed_seconds": round(elapsed, 3),
            "result_text": result.text,
        })

    driver.save_screenshot(str(E / "stable-final.png"))
finally:
    (E / "stable-report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
    driver.quit()

No fixed sleep appears in the stable path. Each run exits as soon as the application phase and run identity satisfy the condition, up to the 2.2-second deadline.

8. Compare before and after without rewriting history

The following table organizes the key choices and evidence for Compare before and after without rewriting history. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.

Evidence Brittle baseline Stabilized run
Synchronization contract 0.5 s wall-clock guess phase=complete + current run ID + result node
Expected delay sensitivity Passes short cases, fails longer cases Passes all configured cases within bounded 2.2 s budget
Implicit wait 0 0
Failure meaning “Result absent after guess” “Required application state not reached before deadline”
First-failure evidence Screenshot on baseline failure Screenshot + page source on timeout
Suite cost Always consumes 0.5 s before lookup Consumes only actual transition time plus polling granularity

Preserve the baseline report even after the stable run succeeds. It demonstrates that the correction changed the synchronization contract, not the AUT timing.

9. Independently verify the predictions

  • Prediction 1: inspect the status immediately after clicking Run; it must be working with the current run ID before completion.
  • Prediction 2: compare baseline pass/fail entries against their configured delays. The pattern should track the 0.5 s guess.
  • Prediction 3: confirm every stable run has passed=true and elapsed time remains below the 2.2 s deadline.
  • Prediction 4: confirm each result and status carry the same current run ID; the custom wait must not accept a prior run.

If a stable run times out even though its configured delay is under 1.4 seconds, inspect the captured screenshot/page source, actual elapsed time, browser/session evidence, and host load before increasing the timeout.

10. Required evidence packet

  • evidence/baseline-*-failure.png for at least the first baseline failures.
  • The printed or saved baseline result list with run IDs, configured delays, pass/fail state, and exception type.
  • evidence/stable-report.json with Selenium/browser/platform/session provenance and per-run elapsed time.
  • evidence/stable-final.png from a successful terminal state.
  • Any stable-*-timeout.png and page source if a stable run fails.
  • Your written predictions and a short conclusion explaining why the condition, not the timeout number, removed the flake pattern.

11. Verification checklist

  • The AUT binds only to loopback and the body build marker is verified.
  • Selenium Python 4.47.0 and actual browser/platform/session identity are recorded.
  • Explicit/custom-wait execution uses implicit wait zero; no mixed-wait policy is present.
  • The baseline contains one intentionally brittle fixed sleep and the stabilized path contains none.
  • At least two state predictions were written before execution and independently checked afterward.
  • The custom wait verifies both terminal application phase and the current run ID.
  • Timeout failures preserve screenshot/page-source evidence before browser teardown.
  • The browser session and local HTTP server are terminated during cleanup.

12. 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 http.server with Ctrl+C first.
deactivate 2>/dev/null || true
cd ..
rm -rf selenium-ch06-checkpoint

Only delete the disposable directory after reviewing or copying the evidence you need. Do not clear shared Selenium Manager caches, browser profiles, unrelated CI workspaces, or production test data as part of this lab.

13. What Chapter 06 adds to the operating model

You can now turn asynchronous browser/AUT behavior into bounded, observable contracts; keep implicit and explicit timing policy understandable; use Python WebDriverWait fluently; create domain-specific conditions; preserve timeout evidence; and distinguish application/infrastructure failure from pure synchronization defects. Chapter 07 builds on that determinism with the Actions API for keyboard, pointer, wheel, and composite input sequences.

Knowledge check

What makes the baseline flaky by design?

Why does the stable condition include run ID as well as phase=complete?

Why should you preserve baseline failures after the stable run passes?

A stable case under the configured delay budget times out in CI. What should you do first?

What concept from this chapter must be applied before Chapter 07 composite actions?

Next lesson

Keyboard, Pointer, Wheel, and Composite Actions API

Apply deterministic readiness checks before coordinated user-input sequences so complex actions are not built on timing races.

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 with ordinary Selenium Manager resolution, and target only loopback fixtures. Python has no separate Java-style FluentWait class: configurable fluent behavior is provided by WebDriverWait(timeout, poll_frequency, ignored_exceptions). Grid, browser clouds, enterprise identity, and BiDi are not required in this chapter. The checkpoint deliberately uses one fixed sleep only in the brittle baseline. The stabilized implementation uses bounded explicit/custom conditions with an implicit wait of zero and preserves first-failure evidence.

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.