
Waitless
The Challenge
UI automation tests suffer from intermittent 'flaky' failures because interactions execute while the UI is still changing. Traditional solutions like time.sleep() are too slow, WebDriverWait only checks one element, and retry decorators mask rather than solve the problem.
The Solution
Built intelligent stability detection using browser-side JavaScript instrumentation. The library monitors DOM mutations (MutationObserver), network requests (XHR/fetch interception), CSS animations, and layout shifts to determine when a page is truly ready for interaction. Integrates with Selenium via a single line of codeβzero test rewrites required.
- βDOM Stability Detection
- βNetwork Idle Monitoring
- βAnimation Completion Tracking
- βOne-Line Integration
- βDetailed Diagnostics
- βZero External Dependencies
Waitless: Case Study
Eliminating Flaky UI Automation Tests Through Intelligent Stability Detection
Executive Summary
Waitless is a Python library that eliminates flaky UI automation test failures by replacing arbitrary waits and sleeps with intelligent stability detection. Through browser-side JavaScript instrumentation, it monitors DOM mutations, network requests, CSS animations, and layout shifts to determine when a page is truly ready for interaction. The library integrates with Selenium via a one-line change, requiring zero modifications to existing test code. This approach reduces test flakiness by addressing the root causeβracing against incomplete UI stateβrather than masking it with arbitrary delays.
Problem
The Original Situation
UI automation tests in large test suites suffer from intermittent failures that pass on retry but fail unpredictably. These "flaky tests" occur because test interactions (clicks, typing, assertions) execute while the UI is still changing.
What Was Broken
Test run 1: β ElementClickInterceptedException
Test run 2: β Pass
Test run 3: β Pass
Test run 4: β StaleElementReferenceException
Test run 5: β Pass
Common failure modes included:
| Failure Type | Root Cause |
|---|---|
ElementClickInterceptedException | Overlay/modal still animating |
StaleElementReferenceException | DOM rebuilt by React/Vue/Angular |
ElementNotInteractableException | Element not yet visible/enabled |
| Wrong element clicked | Layout shift moved target element |
Risks Caused
- Wasted CI time - Re-running flaky tests wastes compute resources
- Lost developer trust - Teams ignore test failures assuming flakiness
- Missed regressions - Real bugs hidden among noise
- Slow feedback loops - Adding arbitrary sleeps slows test execution
Why Existing Approaches Were Insufficient
| Approach | Limitation |
|---|---|
time.sleep(2) | Arbitrary delayβeither too short (still fails) or too long (slows suite) |
WebDriverWait with expected_conditions | Only checks ONE element condition, misses page-wide state |
| Retry decorators | Masks the problem, doesn't solve it; still uses CI time on retries |
| Playwright auto-wait | Framework-specific; doesn't help Selenium users |
None of these approaches addressed the fundamental question: "Is the entire page stable and ready for interaction?"
Challenges
Technical Challenges
-
Defining "stability" - No standard definition exists. What signals indicate a page is ready?
-
Cross-domain monitoring - JavaScript instrumentation must intercept:
- DOM mutations (MutationObserver)
- Network requests (XHR and fetch interception)
- CSS animations/transitions (event listeners)
- Layout changes (ResizeObserver, position tracking)
-
Re-injection after navigation - Single-page apps may destroy instrumentation on route changes
-
Thread safety - Selenium tests may run across multiple threads
-
No external dependencies - Library must work without additional pip packages
Operational Challenges
-
Zero test rewrites - Must integrate without modifying hundreds of existing tests
-
No performance degradation - Cannot add significant overhead to test execution
-
CI compatibility - Must work in headless environments without special setup
Hidden Complexities
-
Infinite animations - Some apps have perpetual spinners that never "stabilize"
-
Background network traffic - Analytics, WebSockets, long-polling never become "idle"
-
Wrapped element identity - Wrapped elements behave like WebElements but
isinstance()returns False
Solution
Design Approach
The solution uses a layered architecture with clear separation of concerns:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Public API β
β stabilize() / unstabilize() / wait_for_stability() β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Selenium Integration Layer β
β StabilizedWebDriver / StabilizedWebElement β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Stabilization Engine β
β Polling, timeout handling, signal evaluation β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β JavaScript Instrumentation β
β MutationObserver, fetch/XHR intercept, animation events β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step-by-Step Implementation
1. Define Stability Signals
Created a signal-based system with mandatory and optional indicators:
| Signal | Type | Threshold | Mandatory |
|---|---|---|---|
| DOM Mutations | MutationObserver | 100ms quiet period | Yes |
| Network Requests | XHR/fetch count | 0 pending | Yes |
| CSS Animations | Event listeners | 0 active | Configurable |
| Layout Shifts | Position tracking | <1px movement | Strict mode |
2. Build JavaScript Instrumentation
Injected script creates a window.__waitless__ object that:
- Intercepts
fetch()andXMLHttpRequestto count pending requests - Registers
MutationObserveron document root - Listens for
animationstart/endandtransitionstart/endevents - Tracks element positions for layout stability
window.__waitless__ = {
pendingRequests: 0,
lastMutationTime: Date.now(),
activeAnimations: 0,
isStable() {
if (this.pendingRequests > 0) return false;
if (Date.now() - this.lastMutationTime < 100) return false;
return true;
}
};
3. Create Stabilization Engine
Python engine that:
- Injects JavaScript via
execute_script() - Polls browser for stability status
- Evaluates signals against configured thresholds
- Re-validates instrumentation before each check (handles navigation)
4. Implement Safe Wrapper Pattern
Instead of monkey-patching Selenium (risky), used wrapper pattern:
class StabilizedWebElement:
def click(self):
self._engine.wait_for_stability() # Auto-wait!
return self._element.click()
This approach:
- Doesn't modify Selenium internals
- Easy to undo with
unstabilize() - Lower risk of breaking on Selenium upgrades
5. Add Diagnostic Reporting
Created waitless doctor CLI that explains WHY stability wasn't reached:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WAITLESS STABILITY REPORT β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β BLOCKING FACTORS: β
β β NETWORK: 2 request(s) still pending β
β β GET /api/users (started 2.3s ago) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β SUGGESTIONS: β
β 1. Set network_idle_threshold=2 for background β
β traffic β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tools & Technologies Used
| Component | Technology |
|---|---|
| Language | Python 3.9+ |
| Browser Integration | Selenium WebDriver |
| Browser Instrumentation | Vanilla JavaScript (injected) |
| Configuration | Python dataclasses |
| CLI | argparse (stdlib) |
| External Dependencies | None |
Package Structure
waitless/
βββ __init__.py # Public API exports
βββ __main__.py # CLI entry point
βββ config.py # StabilizationConfig dataclass
βββ engine.py # Core polling/evaluation logic
βββ exceptions.py # Custom exception types
βββ instrumentation.py # JavaScript code templates
βββ selenium_integration.py # Wrapper classes
βββ signals.py # Signal definitions
βββ diagnostics.py # Report generation
Outcome/Impact
Quantified Improvements
| Metric | Before | After | Improvement |
|---|---|---|---|
| Integration effort | Hours of test rewrites | 1 line of code | ~99% reduction |
| Arbitrary sleeps in tests | Multiple per test | Zero | Eliminated |
| False flaky failures | Common | Rare | Deterministic behavior |
| Diagnostic clarity | "Element not found" | Full stability report | Actionable insights |
Test Code Transformation
Before (brittle):
driver.get("https://example.com")
time.sleep(2) # Hope this is enough?
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "button"))
)
driver.find_element(By.ID, "button").click()
time.sleep(1) # Wait for AJAX?
After (stable):
driver = stabilize(driver) # One-time setup
driver.get("https://example.com")
driver.find_element(By.ID, "button").click() # Just works
Long-Term Benefits
- Reduced CI costs - Fewer flaky re-runs
- Faster test execution - No arbitrary sleeps
- Improved debugging - Clear diagnostics when issues occur
- Framework independence - Core engine can extend to Playwright
- Knowledge capture - Stability definitions codified, not tribal knowledge
Key Files
| File | Purpose |
|---|---|
config.py | Configuration with validation |
engine.py | Core stabilization engine |
instrumentation.py | JavaScript browser monitoring |
selenium_integration.py | Wrapper pattern implementation |
diagnostics.py | Report generation |
README.md | Documentation |
Summary
Waitless solves the pervasive problem of flaky UI tests by replacing time-based waits with intelligent stability detection. Through browser-side JavaScript instrumentation monitoring DOM mutations, network requests, and animations, it determines when a page is truly ready for interaction. The library integrates via a single line of code (stabilize(driver)), requires zero external dependencies, and provides detailed diagnostics when issues occur. This transforms brittle, timing-dependent tests into deterministic, stable automation that works reliably in both local and CI environments.