Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,5 @@ jobs:
python -m venv wheel-smoke
source wheel-smoke/bin/activate
python -m pip install dist/*.whl
python -c "import selenium, waitless; assert waitless.__version__ == '1.0.1'; print(waitless.__version__, selenium.__version__)"
python -c "import selenium, waitless; assert waitless.__version__ == '1.0.3'; print(waitless.__version__, selenium.__version__)"

4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.1] - Unreleased
## [1.0.3] - 2026-08-30

### Fixed
- Balance XHR request accounting when a synchronous `send()` fails before `loadend`.
- Tighten README and package claims to the behavior verified by the implementation and tests.
- Propagate Python stabilization options into browser instrumentation before it initializes.
- Count individual DOM mutation records, including Shadow DOM mutations.
- Execute configured React, Angular, and Vue adapter detection/status hooks.
Expand Down
61 changes: 34 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ from selenium import webdriver
from selenium.webdriver.common.by import By
from waitless import stabilize

# Create driver as usual
driver = webdriver.Chrome()

# Enable automatic stabilization - ONE LINE
driver = stabilize(driver)

# All interactions now auto-wait for stability
driver.get("https://example.com")
driver.find_element(By.ID, "login-button").click() # ← Auto-waits!
driver.find_element(By.ID, "username").send_keys("user") # ← Auto-waits!
# Navigation, lookups, and common element actions now wait for stability.
driver.get(
"data:text/html,<input id='username'><button id='login-button'>Log in</button>"
)
driver.find_element(By.ID, "username").send_keys("user")
driver.find_element(By.ID, "login-button").click()
driver.quit()
```

## Why Waitless?
Expand All @@ -49,22 +49,23 @@ Automation tests fail because interactions happen while the UI is still changing
| Approach | Problem |
|----------|---------|
| `time.sleep(2)` | Too slow, still fails sometimes |
| `WebDriverWait` | Only checks one element, misses page-wide state |
| `WebDriverWait` | Requires an explicit condition at each synchronization point |
| Retries | Masks the real problem, adds flakiness |

### The Waitless Solution

Waitless monitors the **entire page** for stability signals:
Waitless evaluates page-level stability signals:

- DOM mutation activity (MutationObserver, including **Shadow DOM**)
- Pending network requests (XHR/fetch interception)
- CSS animations and transitions
- Layout stability (element movement)
- WebSocket/SSE activity (opt-in)
- Framework hooks (React/Angular/Vue, opt-in)
- Same-origin iframe load readiness (opt-in; not full child-frame signal injection)
- DOM mutation activity (MutationObserver, including **Shadow DOM**)
- Pending network requests (XHR/fetch interception after instrumentation is installed)
- CSS animations and transitions
- Layout stability for interactive elements
- WebSocket/SSE activity (opt-in)
- Framework hooks (React/Angular/Vue, opt-in)
- Same-origin iframe load readiness (opt-in; not full child-frame signal injection)

When you interact, waitless ensures the page is truly ready.
Before supported interactions, Waitless polls until the enabled mandatory signals
meet their configured thresholds.

## Configuration

Expand Down Expand Up @@ -172,14 +173,15 @@ Many apps have background traffic that never stops:
- Feature flags
- WebSocket heartbeats

If tests timeout frequently, try:
If known background traffic exceeds the default, raise the threshold carefully:
```python
config = StabilizationConfig(network_idle_threshold=2)
config = StabilizationConfig(network_idle_threshold=3)
```

### Wrapped Elements

The stabilized driver returns wrapped elements that auto-wait. They behave like WebElements but:
The stabilized driver returns wrapped elements that auto-wait before `click()`,
`send_keys()`, `submit()`, and `clear()`. They behave like WebElements but:

- `isinstance(element, WebElement)` returns `False`
- Use `.unwrap()` to get the original element if needed
Expand All @@ -189,15 +191,14 @@ element = driver.find_element(By.ID, "button")
original = element.unwrap() # Gets the real WebElement
```

## v1.0.0 New Features
## Optional Signals

- **WebSocket/SSE Awareness** - Track WebSocket and Server-Sent Events activity
- **Framework Adapters** - React, Angular, Vue hooks for framework-specific settling
- **iframe Support** - Monitor same-origin iframes
- **Performance Benchmarks** - Built-in benchmark suite
- **Performance benchmarks** - Run the repository benchmark against your environment

```python
# Enable new v1.0 features
config = StabilizationConfig(
track_websocket=True, # WebSocket monitoring
track_sse=True, # SSE monitoring
Expand All @@ -210,10 +211,10 @@ config = StabilizationConfig(

| Metric | Typical Value |
|--------|---------------|
| Instrumentation injection | Environment-dependent; run `python -m benchmarks.overhead_test` |
| Per-poll overhead | Environment-dependent; run the bundled benchmark |
| Instrumentation injection | Environment-dependent; from a repository checkout, run `python benchmarks/overhead_test.py` |
| Per-poll overhead | Environment-dependent; run the repository benchmark |
| Poll interval (default) | 50ms |
| Typical stabilization | 50-200ms after activity |
| Stabilization time | Depends on page activity, thresholds, and environment |

### Navigation Handling

Expand All @@ -228,12 +229,18 @@ JavaScript, Waitless validates/re-injects instrumentation on the next wait:
This does not observe routes continuously, and cross-origin iframe internals remain
outside the browser same-origin boundary.

Because instrumentation is installed after Selenium's synchronous navigation call
returns, requests that start and finish during navigation are not observed. Requests
started after instrumentation is installed are tracked.

`find_elements()` keeps Selenium's immediate-empty lookup semantics: after the
page-stability wait, it performs one lookup and returns `[]` when there are no matches.
By contrast, `find_element()` retries `NoSuchElementException` until the configured
timeout after the page-stability wait.

## Current Limitations

- **Selenium only** - Playwright support planned
- **Selenium only** - No Playwright integration
- **Sync only** - No async/await support yet
- **No Service Workers** - SW network requests not intercepted

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"

[project]
name = "waitless"
version = "1.0.1"
description = "Eliminate explicit waits in UI automation by detecting true UI stability"
version = "1.0.3"
description = "Reduce explicit waits in Selenium by evaluating UI stability signals"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
Expand Down
26 changes: 26 additions & 0 deletions tests/integration/test_selenium_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,32 @@ def test_mutation_rate_counts_records_not_callbacks(self, driver, fixture_url):
)
assert rate >= 100

def test_failed_xhr_send_does_not_leave_pending_request(self, driver, fixture_url):
driver.get(fixture_url)
wrapped = stabilize(driver)
wrapped._engine.ensure_instrumented()

result = driver.execute_script(
"""
const xhr = new XMLHttpRequest();
let errorName = null;
try {
xhr.send();
} catch (error) {
errorName = error.name;
}
return {
errorName: errorName,
pendingRequests: window.__waitless__.pendingRequests
};
"""
)

assert result == {
"errorName": "InvalidStateError",
"pendingRequests": 0,
}

def test_framework_adapter_detection_and_status_are_executed(self, driver, fixture_url):
driver.get(fixture_url)
driver.execute_script(
Expand Down
8 changes: 4 additions & 4 deletions waitless/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""
Waitless - Zero-wait UI automation stabilization library.
Waitless - Automatic Selenium UI stabilization library.

Eliminate explicit waits and sleeps in UI automation by automatically
waiting for true UI stability instead of time-based conditions.
Reduce explicit waits and sleeps by evaluating configurable UI stability
signals before supported Selenium interactions.

Basic Usage:
from waitless import stabilize
Expand Down Expand Up @@ -36,7 +36,7 @@
driver = unstabilize(driver) # Back to original behavior
"""

__version__ = '1.0.1'
__version__ = '1.0.3'
__author__ = 'Dhiraj Das'

# Public API
Expand Down
2 changes: 1 addition & 1 deletion waitless/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
prog='waitless',
description='Waitless - Zero-wait UI automation stabilization'
description='Waitless - Automatic Selenium UI stabilization'
)

subparsers = parser.add_subparsers(dest='command', help='Available commands')
Expand Down
3 changes: 2 additions & 1 deletion waitless/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ class StabilizationConfig:
Options: 'react', 'angular', 'vue'. Default empty (auto-detect off).
When specified, waitless will inject framework-specific hooks.

track_iframes: Whether to inject instrumentation into same-origin iframes.
track_iframes: Whether to monitor same-origin iframe load readiness.
This does not inject full child-frame instrumentation.
Default False. Cross-origin iframes cannot be accessed.
"""

Expand Down
14 changes: 11 additions & 3 deletions waitless/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

window.__waitless__ = {
_initialized: true,
_version: '1.0.1',
_version: '1.0.3',

// State tracking
pendingRequests: 0,
Expand Down Expand Up @@ -249,8 +249,16 @@
xhr.addEventListener('loadend', function() {
self._requestEnded(url, 'xhr', xhr.status);
});

return self._originalXHRSend.apply(this, arguments);

try {
return self._originalXHRSend.apply(this, arguments);
} catch (error) {
// Synchronous failures (for example, send() before open()) do
// not emit loadend. Balance the counter before preserving the
// browser's original exception behavior.
self._requestEnded(url, 'xhr', 'error');
throw error;
}
};
},

Expand Down
Loading