Individual wait techniques are tools. A wait strategy is the plan for how you apply those tools consistently across your entire framework. A good strategy eliminates the need to think about synchronisation in every test — the framework handles it through BasePage methods, page object constructors, and convention. This lesson presents a comprehensive strategy that, when applied consistently, produces a test suite with near-zero flakiness.
A Comprehensive Wait Strategy for Flake-Free Testing
The strategy has four layers: BasePage wraps all element interactions with appropriate waits, page objects verify page load in their constructors, tests use high-level page object methods instead of raw Selenium, and a fallback diagnostic layer captures evidence when waits fail.
# ── The Four-Layer Wait Strategy ──
WAIT_STRATEGY = {
"Layer 1 — BasePage Wrapped Methods": {
"principle": "Every element interaction goes through a BasePage method that includes the appropriate wait",
"implementation": [
"click(locator) → wait for element_to_be_clickable, then click",
"type_text(locator) → wait for visibility_of_element_located, then clear+type",
"get_text(locator) → wait for visibility_of_element_located, then read text",
"is_displayed(loc) → wait for visibility with short timeout, return bool",
],
"benefit": "Tests never call raw find_element — every interaction has a built-in wait",
},
"Layer 2 — Page Object Constructors": {
"principle": "Every page object verifies it landed on the correct page",
"implementation": [
"InventoryPage.__init__ → wait for inventory_list visible",
"CartPage.__init__ → wait for cart_contents visible",
"CheckoutPage.__init__ → wait for checkout_form visible",
],
"benefit": "Navigation failures caught immediately with clear error messages",
},
"Layer 3 — Test-Level Synchronisation": {
"principle": "Tests use page object methods, not raw waits or sleeps",
"implementation": [
"login_page.login(u, p) → returns InventoryPage (includes wait in constructor)",
"inventory.add_to_cart(0) → includes wait for button clickability",
"cart.get_total() → includes wait for total element visibility",
],
"benefit": "Tests read as business actions with zero synchronisation code",
},
"Layer 4 — Diagnostic Fallback": {
"principle": "When a wait fails, capture evidence for debugging",
"implementation": [
"On TimeoutException → take screenshot",
"On TimeoutException → capture page source",
"On TimeoutException → log current URL and browser console errors",
"Attach evidence to test report (Allure, pytest-html)",
],
"benefit": "Failed wait = full diagnostic package instead of just an error message",
},
}
# Rules for choosing wait conditions
WAIT_RULES = [
{"action": "Click a button/link", "wait_for": "element_to_be_clickable"},
{"action": "Type into an input field", "wait_for": "visibility_of_element_located"},
{"action": "Read text from an element", "wait_for": "visibility_of_element_located"},
{"action": "Verify element is gone", "wait_for": "invisibility_of_element_located"},
{"action": "Page navigation completed", "wait_for": "url_contains or key element visible"},
{"action": "Loading spinner disappeared", "wait_for": "invisibility_of_element_located"},
{"action": "Dynamic content loaded", "wait_for": "Custom condition (row count, text change)"},
{"action": "Iframe content ready", "wait_for": "frame_to_be_available_and_switch_to_it"},
{"action": "Popup window opened", "wait_for": "number_of_windows_to_be"},
]
print("Four-Layer Wait Strategy")
print("=" * 65)
for layer, info in WAIT_STRATEGY.items():
print(f"\n {layer}")
print(f" Principle: {info['principle']}")
print(f" Benefit: {info['benefit']}")
print("\n\nWait Condition Quick Reference:")
print("=" * 65)
print(f" {'Action':<40} {'Wait For'}")
print(f" {'-'*65}")
for rule in WAIT_RULES:
print(f" {rule['action']:<40} {rule['wait_for']}")
WebDriverWait or time.sleep() directly in their test methods. All synchronisation is handled by BasePage (Layer 1) and page object constructors (Layer 2). Tests only call high-level methods like login() and add_to_cart() (Layer 3). This separation means a junior tester can write new tests without understanding Selenium's wait API — they simply use page object methods, and the waits happen automatically under the hood.request.node.rep_call.failed hook or a custom wrapper around WebDriverWait that catches TimeoutException, takes a screenshot, and re-raises the exception with the screenshot path in the error message. This turns a cryptic "TimeoutException: waiting for element_to_be_clickable" into "TimeoutException — screenshot saved: reports/screenshots/test_checkout_fails.png" — dramatically reducing debugging time.Common Mistakes
Mistake 1 — Implementing waits inconsistently across the framework
❌ Wrong: Some page objects use explicit waits, some use implicit waits, some use time.sleep, and some have no waits at all.
✅ Correct: All page objects inherit from BasePage, which provides wrapped methods with consistent explicit waits. No test or page object uses time.sleep or raw find_element. The entire framework uses one synchronisation approach.
Mistake 2 — Not capturing diagnostic evidence when waits fail
❌ Wrong: A CI failure shows "TimeoutException: Message:" with no additional context — the developer must reproduce the failure locally to understand what happened.
✅ Correct: The framework automatically captures a screenshot, page source, current URL, and browser console logs on every wait failure. The CI report includes these artefacts, enabling diagnosis without reproduction.