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.
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.
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
workingwith 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=trueand 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.pngfor 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.jsonwith Selenium/browser/platform/session provenance and per-run elapsed time. -
evidence/stable-final.pngfrom a successful terminal state. -
Any
stable-*-timeout.pngand 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?
It checks after a fixed 0.5-second sleep while the fixture uses several delays both below and above that value, so pass/fail follows timing luck rather than readiness.
Why does the stable condition include run ID as well as phase=complete?
It prevents a result from a previous run or unrelated transition from satisfying the current wait.
Why should you preserve baseline failures after the stable run passes?
They prove the original synchronization defect and show that the repair changed the wait contract rather than hiding evidence.
A stable case under the configured delay budget times out in CI. What should you do first?
Preserve the timeout evidence and inspect actual AUT state, session/browser versions, host/Grid/resource conditions, and elapsed timing before increasing the deadline.
What concept from this chapter must be applied before Chapter 07 composite actions?
Wait for the specific observable state required by the next input action instead of starting pointer/keyboard sequences against a page that may still be transitioning.
Official references and version notes
- Selenium 4.47 release — current pinned Selenium release baseline for this chapter.
- Waiting Strategies — current Selenium guidance on page-load readiness, implicit waits, explicit waits, and the warning against mixing implicit and explicit waits.
- Waiting with Expected Conditions — common explicit-wait conditions such as presence, visibility, stale state, text, and title checks.
-
Python WebDriverWait API 4.47.0
— timeout, poll frequency, ignored exceptions,
until, anduntil_not. - Python Timeouts API 4.47.0 — implicit, page-load, and script timeout concepts.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.