
Waitless
$ pip install waitlessThe 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 browser-side instrumentation for configurable DOM and network stability checks, with optional animation and layout signals. Diagnostics explain which signals are still active. Application-specific waits and outcome assertions remain part of the test.
Browser Stability Sensor Network
Waitless replaces sleep-based timing with browser-native signals that describe when the page is actually ready.
- ✓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.