waitless 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- waitless-0.3.0/LICENSE +21 -0
- {waitless-0.1.0 → waitless-0.3.0}/PKG-INFO +17 -11
- {waitless-0.1.0 → waitless-0.3.0}/README.md +12 -8
- {waitless-0.1.0 → waitless-0.3.0}/pyproject.toml +3 -3
- {waitless-0.1.0 → waitless-0.3.0}/waitless/__init__.py +1 -1
- {waitless-0.1.0 → waitless-0.3.0}/waitless/config.py +14 -14
- {waitless-0.1.0 → waitless-0.3.0}/waitless/engine.py +22 -3
- {waitless-0.1.0 → waitless-0.3.0}/waitless/instrumentation.py +35 -2
- {waitless-0.1.0 → waitless-0.3.0}/waitless/selenium_integration.py +81 -6
- {waitless-0.1.0 → waitless-0.3.0}/waitless/signals.py +34 -5
- {waitless-0.1.0 → waitless-0.3.0}/waitless.egg-info/PKG-INFO +17 -11
- {waitless-0.1.0 → waitless-0.3.0}/waitless.egg-info/SOURCES.txt +1 -0
- {waitless-0.1.0 → waitless-0.3.0}/setup.cfg +0 -0
- {waitless-0.1.0 → waitless-0.3.0}/waitless/__main__.py +0 -0
- {waitless-0.1.0 → waitless-0.3.0}/waitless/diagnostics.py +0 -0
- {waitless-0.1.0 → waitless-0.3.0}/waitless/exceptions.py +0 -0
- {waitless-0.1.0 → waitless-0.3.0}/waitless.egg-info/dependency_links.txt +0 -0
- {waitless-0.1.0 → waitless-0.3.0}/waitless.egg-info/entry_points.txt +0 -0
- {waitless-0.1.0 → waitless-0.3.0}/waitless.egg-info/requires.txt +0 -0
- {waitless-0.1.0 → waitless-0.3.0}/waitless.egg-info/top_level.txt +0 -0
waitless-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dhiraj Das
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Eliminate explicit waits in UI automation by detecting true UI stability
|
|
5
|
-
Author-email: Dhiraj Das <dhirajdas.
|
|
5
|
+
Author-email: Dhiraj Das <dhirajdas.666@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://www.dhirajdas.dev
|
|
8
8
|
Project-URL: Documentation, https://github.com/godhiraj-code/waitless#readme
|
|
9
9
|
Project-URL: Repository, https://github.com/godhiraj-code/waitless.git
|
|
10
10
|
Project-URL: Issues, https://github.com/godhiraj-code/waitless/issues
|
|
@@ -22,12 +22,16 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
28
|
Requires-Dist: selenium>=4.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
28
30
|
|
|
29
31
|
# Waitless
|
|
30
32
|
|
|
33
|
+
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
34
|
+
|
|
31
35
|
**Zero-wait UI automation stabilization for Selenium**
|
|
32
36
|
|
|
33
37
|
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
@@ -93,12 +97,12 @@ When you interact, waitless ensures the page is truly ready.
|
|
|
93
97
|
from waitless import stabilize, StabilizationConfig
|
|
94
98
|
|
|
95
99
|
config = StabilizationConfig(
|
|
96
|
-
timeout=
|
|
97
|
-
|
|
98
|
-
network_idle_threshold=
|
|
99
|
-
animation_detection=True,
|
|
100
|
-
strictness='normal',
|
|
101
|
-
debug_mode=True
|
|
100
|
+
timeout=10, # Max wait time (seconds)
|
|
101
|
+
mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
|
|
102
|
+
network_idle_threshold=2, # Max pending requests (allows background traffic)
|
|
103
|
+
animation_detection=True, # Track CSS animations (non-blocking in normal mode)
|
|
104
|
+
strictness='normal', # 'strict' | 'normal' | 'relaxed'
|
|
105
|
+
debug_mode=True # Enable logging
|
|
102
106
|
)
|
|
103
107
|
|
|
104
108
|
driver = stabilize(driver, config=config)
|
|
@@ -185,7 +189,7 @@ Sample output:
|
|
|
185
189
|
|
|
186
190
|
### Network Threshold Warning
|
|
187
191
|
|
|
188
|
-
The default `network_idle_threshold=
|
|
192
|
+
The default `network_idle_threshold=2` allows some background traffic.
|
|
189
193
|
|
|
190
194
|
Many apps have background traffic that never stops:
|
|
191
195
|
- Analytics calls
|
|
@@ -210,7 +214,7 @@ element = driver.find_element(By.ID, "button")
|
|
|
210
214
|
original = element.unwrap() # Gets the real WebElement
|
|
211
215
|
```
|
|
212
216
|
|
|
213
|
-
## v0 Limitations
|
|
217
|
+
## v0.3 Limitations
|
|
214
218
|
|
|
215
219
|
- **Selenium only** - Playwright support planned for v1
|
|
216
220
|
- **Sync only** - No async/await support yet
|
|
@@ -218,6 +222,8 @@ original = element.unwrap() # Gets the real WebElement
|
|
|
218
222
|
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
219
223
|
- **No Service Workers** - SW network requests not intercepted
|
|
220
224
|
|
|
225
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
226
|
+
|
|
221
227
|
## API Reference
|
|
222
228
|
|
|
223
229
|
### Functions
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Waitless
|
|
2
2
|
|
|
3
|
+
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
**Zero-wait UI automation stabilization for Selenium**
|
|
4
6
|
|
|
5
7
|
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
@@ -65,12 +67,12 @@ When you interact, waitless ensures the page is truly ready.
|
|
|
65
67
|
from waitless import stabilize, StabilizationConfig
|
|
66
68
|
|
|
67
69
|
config = StabilizationConfig(
|
|
68
|
-
timeout=
|
|
69
|
-
|
|
70
|
-
network_idle_threshold=
|
|
71
|
-
animation_detection=True,
|
|
72
|
-
strictness='normal',
|
|
73
|
-
debug_mode=True
|
|
70
|
+
timeout=10, # Max wait time (seconds)
|
|
71
|
+
mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
|
|
72
|
+
network_idle_threshold=2, # Max pending requests (allows background traffic)
|
|
73
|
+
animation_detection=True, # Track CSS animations (non-blocking in normal mode)
|
|
74
|
+
strictness='normal', # 'strict' | 'normal' | 'relaxed'
|
|
75
|
+
debug_mode=True # Enable logging
|
|
74
76
|
)
|
|
75
77
|
|
|
76
78
|
driver = stabilize(driver, config=config)
|
|
@@ -157,7 +159,7 @@ Sample output:
|
|
|
157
159
|
|
|
158
160
|
### Network Threshold Warning
|
|
159
161
|
|
|
160
|
-
The default `network_idle_threshold=
|
|
162
|
+
The default `network_idle_threshold=2` allows some background traffic.
|
|
161
163
|
|
|
162
164
|
Many apps have background traffic that never stops:
|
|
163
165
|
- Analytics calls
|
|
@@ -182,7 +184,7 @@ element = driver.find_element(By.ID, "button")
|
|
|
182
184
|
original = element.unwrap() # Gets the real WebElement
|
|
183
185
|
```
|
|
184
186
|
|
|
185
|
-
## v0 Limitations
|
|
187
|
+
## v0.3 Limitations
|
|
186
188
|
|
|
187
189
|
- **Selenium only** - Playwright support planned for v1
|
|
188
190
|
- **Sync only** - No async/await support yet
|
|
@@ -190,6 +192,8 @@ original = element.unwrap() # Gets the real WebElement
|
|
|
190
192
|
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
191
193
|
- **No Service Workers** - SW network requests not intercepted
|
|
192
194
|
|
|
195
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
196
|
+
|
|
193
197
|
## API Reference
|
|
194
198
|
|
|
195
199
|
### Functions
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "waitless"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Eliminate explicit waits in UI automation by detecting true UI stability"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
12
|
authors = [
|
|
13
|
-
{name = "Dhiraj Das", email = "dhirajdas.
|
|
13
|
+
{name = "Dhiraj Das", email = "dhirajdas.666@gmail.com"}
|
|
14
14
|
]
|
|
15
15
|
keywords = [
|
|
16
16
|
"selenium",
|
|
@@ -36,7 +36,7 @@ classifiers = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
39
|
-
Homepage = "https://
|
|
39
|
+
Homepage = "https://www.dhirajdas.dev"
|
|
40
40
|
Documentation = "https://github.com/godhiraj-code/waitless#readme"
|
|
41
41
|
Repository = "https://github.com/godhiraj-code/waitless.git"
|
|
42
42
|
Issues = "https://github.com/godhiraj-code/waitless/issues"
|
|
@@ -22,18 +22,16 @@ class StabilizationConfig:
|
|
|
22
22
|
Consider lowering to 5s for faster feedback loops.
|
|
23
23
|
|
|
24
24
|
dom_settle_time: Time (seconds) DOM must be quiet to be considered stable.
|
|
25
|
-
Default 0.1s (100ms).
|
|
25
|
+
Default 0.1s (100ms). This is a FALLBACK - mutation rate
|
|
26
|
+
is the primary check for DOM stability.
|
|
27
|
+
|
|
28
|
+
mutation_rate_threshold: Maximum mutations per second for DOM to be stable.
|
|
29
|
+
Default 50/sec. This allows animated sites (typewriter
|
|
30
|
+
effects ~30/sec) while catching loading bursts (100+/sec).
|
|
26
31
|
|
|
27
32
|
network_idle_threshold: Maximum pending requests allowed for stability.
|
|
28
|
-
Default
|
|
29
|
-
|
|
30
|
-
⚠️ WARNING: Many apps have background traffic:
|
|
31
|
-
- Analytics calls
|
|
32
|
-
- Long polling
|
|
33
|
-
- Feature flags
|
|
34
|
-
- WebSocket heartbeats
|
|
35
|
-
|
|
36
|
-
If your tests timeout frequently, try setting this to 1-2.
|
|
33
|
+
Default 2 (allows background analytics/polling).
|
|
34
|
+
Set to 0 for strict mode if all requests must complete.
|
|
37
35
|
|
|
38
36
|
animation_detection: Whether to wait for CSS animations/transitions.
|
|
39
37
|
Default True. Disable for apps with infinite animations.
|
|
@@ -42,9 +40,9 @@ class StabilizationConfig:
|
|
|
42
40
|
Default True in 'strict' mode.
|
|
43
41
|
|
|
44
42
|
strictness: Overall strictness level.
|
|
45
|
-
- 'strict': All signals must be stable (recommended)
|
|
46
|
-
- 'normal': DOM + Network only (
|
|
47
|
-
- 'relaxed': DOM only (fastest
|
|
43
|
+
- 'strict': All signals must be stable (recommended for CI)
|
|
44
|
+
- 'normal': DOM + Network only (default)
|
|
45
|
+
- 'relaxed': DOM only (fastest)
|
|
48
46
|
|
|
49
47
|
debug_mode: Enable verbose logging for troubleshooting.
|
|
50
48
|
Default False.
|
|
@@ -58,7 +56,8 @@ class StabilizationConfig:
|
|
|
58
56
|
|
|
59
57
|
timeout: float = 10.0
|
|
60
58
|
dom_settle_time: float = 0.1
|
|
61
|
-
|
|
59
|
+
mutation_rate_threshold: float = 50.0 # mutations/sec - allows animations
|
|
60
|
+
network_idle_threshold: int = 2 # Allow background traffic
|
|
62
61
|
animation_detection: bool = True
|
|
63
62
|
layout_stability: bool = True
|
|
64
63
|
strictness: StrictnessLevel = 'normal'
|
|
@@ -118,6 +117,7 @@ class StabilizationConfig:
|
|
|
118
117
|
current = {
|
|
119
118
|
'timeout': self.timeout,
|
|
120
119
|
'dom_settle_time': self.dom_settle_time,
|
|
120
|
+
'mutation_rate_threshold': self.mutation_rate_threshold,
|
|
121
121
|
'network_idle_threshold': self.network_idle_threshold,
|
|
122
122
|
'animation_detection': self.animation_detection,
|
|
123
123
|
'layout_stability': self.layout_stability,
|
|
@@ -10,6 +10,21 @@ import threading
|
|
|
10
10
|
import logging
|
|
11
11
|
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
12
12
|
|
|
13
|
+
# Import Selenium exceptions for specific error handling
|
|
14
|
+
try:
|
|
15
|
+
from selenium.common.exceptions import (
|
|
16
|
+
WebDriverException,
|
|
17
|
+
JavascriptException,
|
|
18
|
+
NoSuchWindowException,
|
|
19
|
+
)
|
|
20
|
+
SELENIUM_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
# Fallback for when selenium is not installed
|
|
23
|
+
WebDriverException = Exception
|
|
24
|
+
JavascriptException = Exception
|
|
25
|
+
NoSuchWindowException = Exception
|
|
26
|
+
SELENIUM_AVAILABLE = False
|
|
27
|
+
|
|
13
28
|
from .config import StabilizationConfig, DEFAULT_CONFIG
|
|
14
29
|
from .signals import SignalEvaluator, StabilityStatus
|
|
15
30
|
from .instrumentation import (
|
|
@@ -53,6 +68,7 @@ class StabilizationEngine:
|
|
|
53
68
|
|
|
54
69
|
|
|
55
70
|
self._last_status: Optional[StabilityStatus] = None
|
|
71
|
+
self._last_browser_state: Optional[Dict[str, Any]] = None
|
|
56
72
|
self._last_blocking_factors: Dict[str, Any] = {}
|
|
57
73
|
self._timeline: list = []
|
|
58
74
|
|
|
@@ -85,7 +101,8 @@ class StabilizationEngine:
|
|
|
85
101
|
"""Get current page URL safely."""
|
|
86
102
|
try:
|
|
87
103
|
return self.driver.current_url
|
|
88
|
-
except
|
|
104
|
+
except (WebDriverException, NoSuchWindowException) as e:
|
|
105
|
+
self._debug(f"Could not get current URL: {e}")
|
|
89
106
|
return ""
|
|
90
107
|
|
|
91
108
|
def _is_instrumentation_alive(self) -> bool:
|
|
@@ -98,7 +115,8 @@ class StabilizationEngine:
|
|
|
98
115
|
try:
|
|
99
116
|
result = self.driver.execute_script(CHECK_ALIVE_SCRIPT)
|
|
100
117
|
return result is True
|
|
101
|
-
except
|
|
118
|
+
except (JavascriptException, WebDriverException, NoSuchWindowException) as e:
|
|
119
|
+
self._debug(f"Instrumentation check failed: {e}")
|
|
102
120
|
return False
|
|
103
121
|
|
|
104
122
|
def _inject_instrumentation(self) -> None:
|
|
@@ -175,6 +193,7 @@ class StabilizationEngine:
|
|
|
175
193
|
status = self.evaluator.evaluate(browser_state, current_time)
|
|
176
194
|
last_status = status
|
|
177
195
|
self._last_status = status
|
|
196
|
+
self._last_browser_state = browser_state # Store for diagnostics
|
|
178
197
|
|
|
179
198
|
if status.is_stable:
|
|
180
199
|
self._debug(f"UI stable after {elapsed:.2f}s")
|
|
@@ -260,7 +279,7 @@ class StabilizationEngine:
|
|
|
260
279
|
'network_idle_threshold': self.config.network_idle_threshold,
|
|
261
280
|
'animation_detection': self.config.animation_detection,
|
|
262
281
|
},
|
|
263
|
-
'last_status': self.
|
|
282
|
+
'last_status': self._last_browser_state, # Raw browser state with mutation_rate, etc.
|
|
264
283
|
'blocking_factors': self._last_blocking_factors,
|
|
265
284
|
'timeline': self._timeline[-50:],
|
|
266
285
|
'instrumented': self._instrumented,
|
|
@@ -68,11 +68,26 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
68
68
|
|
|
69
69
|
// ===== MUTATION OBSERVER =====
|
|
70
70
|
|
|
71
|
+
// Rolling window for mutation rate calculation
|
|
72
|
+
_mutationTimestamps: [],
|
|
73
|
+
_mutationWindowMs: 1000, // 1 second window for rate calculation
|
|
74
|
+
|
|
71
75
|
_setupMutationObserver: function() {
|
|
72
76
|
var self = this;
|
|
73
77
|
var observer = new MutationObserver(function(mutations) {
|
|
74
|
-
|
|
75
|
-
self.
|
|
78
|
+
var now = Date.now();
|
|
79
|
+
self.lastMutationTime = now;
|
|
80
|
+
|
|
81
|
+
// Add to rolling window
|
|
82
|
+
self._mutationTimestamps.push(now);
|
|
83
|
+
|
|
84
|
+
// Remove old timestamps outside the window
|
|
85
|
+
var cutoff = now - self._mutationWindowMs;
|
|
86
|
+
while (self._mutationTimestamps.length > 0 && self._mutationTimestamps[0] < cutoff) {
|
|
87
|
+
self._mutationTimestamps.shift();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
self._log('DOM mutation', { count: mutations.length, rate: self.getMutationRate() });
|
|
76
91
|
});
|
|
77
92
|
|
|
78
93
|
observer.observe(document.documentElement || document.body, {
|
|
@@ -85,6 +100,23 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
85
100
|
this._observers.push(observer);
|
|
86
101
|
},
|
|
87
102
|
|
|
103
|
+
// Calculate mutations per second from rolling window
|
|
104
|
+
getMutationRate: function() {
|
|
105
|
+
var now = Date.now();
|
|
106
|
+
var cutoff = now - this._mutationWindowMs;
|
|
107
|
+
|
|
108
|
+
// Count mutations in the last second
|
|
109
|
+
var count = 0;
|
|
110
|
+
for (var i = 0; i < this._mutationTimestamps.length; i++) {
|
|
111
|
+
if (this._mutationTimestamps[i] > cutoff) {
|
|
112
|
+
count++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Return rate per second
|
|
117
|
+
return count;
|
|
118
|
+
},
|
|
119
|
+
|
|
88
120
|
// ===== NETWORK INTERCEPTORS =====
|
|
89
121
|
|
|
90
122
|
_setupNetworkInterceptors: function() {
|
|
@@ -246,6 +278,7 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
246
278
|
stable: this.isStable(),
|
|
247
279
|
pending_requests: this.pendingRequests,
|
|
248
280
|
last_mutation_time: this.lastMutationTime,
|
|
281
|
+
mutation_rate: this.getMutationRate(), // mutations per second
|
|
249
282
|
active_animations: this.activeAnimations + this.activeTransitions,
|
|
250
283
|
layout_shifting: this.layoutShifting,
|
|
251
284
|
pending_request_details: this.pendingRequestDetails.slice(),
|
|
@@ -116,14 +116,74 @@ class StabilizedWebDriver:
|
|
|
116
116
|
return attr
|
|
117
117
|
|
|
118
118
|
def _stabilized_find_element(self, *args, **kwargs) -> StabilizedWebElement:
|
|
119
|
-
"""
|
|
120
|
-
element
|
|
121
|
-
|
|
119
|
+
"""
|
|
120
|
+
Find element with automatic waiting.
|
|
121
|
+
|
|
122
|
+
This method:
|
|
123
|
+
1. Waits for page stability first
|
|
124
|
+
2. Tries to find the element
|
|
125
|
+
3. If element not found, retries until timeout
|
|
126
|
+
|
|
127
|
+
This eliminates the need for explicit WebDriverWait in test code.
|
|
128
|
+
"""
|
|
129
|
+
import time
|
|
130
|
+
from selenium.common.exceptions import NoSuchElementException
|
|
131
|
+
|
|
132
|
+
timeout = self._engine.config.timeout
|
|
133
|
+
poll_interval = self._engine.config.poll_interval
|
|
134
|
+
start_time = time.time()
|
|
135
|
+
last_exception = None
|
|
136
|
+
|
|
137
|
+
while (time.time() - start_time) < timeout:
|
|
138
|
+
# Wait for page stability first
|
|
139
|
+
try:
|
|
140
|
+
self._engine.wait_for_stability()
|
|
141
|
+
except Exception:
|
|
142
|
+
pass # Continue trying to find element
|
|
143
|
+
|
|
144
|
+
# Try to find the element
|
|
145
|
+
try:
|
|
146
|
+
element = self._driver.find_element(*args, **kwargs)
|
|
147
|
+
return StabilizedWebElement(element, self._engine)
|
|
148
|
+
except NoSuchElementException as e:
|
|
149
|
+
last_exception = e
|
|
150
|
+
# Element not found - wait and retry
|
|
151
|
+
time.sleep(poll_interval)
|
|
152
|
+
|
|
153
|
+
# Timeout reached - raise the last exception
|
|
154
|
+
if last_exception:
|
|
155
|
+
raise last_exception
|
|
156
|
+
raise NoSuchElementException(f"Element not found within {timeout}s: {args}")
|
|
122
157
|
|
|
123
158
|
def _stabilized_find_elements(self, *args, **kwargs) -> List[StabilizedWebElement]:
|
|
124
|
-
"""
|
|
125
|
-
elements
|
|
126
|
-
|
|
159
|
+
"""
|
|
160
|
+
Find elements with automatic waiting.
|
|
161
|
+
|
|
162
|
+
Similar to find_element but returns list (may be empty).
|
|
163
|
+
"""
|
|
164
|
+
import time
|
|
165
|
+
|
|
166
|
+
timeout = self._engine.config.timeout
|
|
167
|
+
poll_interval = self._engine.config.poll_interval
|
|
168
|
+
start_time = time.time()
|
|
169
|
+
|
|
170
|
+
while (time.time() - start_time) < timeout:
|
|
171
|
+
# Wait for stability first
|
|
172
|
+
try:
|
|
173
|
+
self._engine.wait_for_stability()
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
# Try to find elements
|
|
178
|
+
elements = self._driver.find_elements(*args, **kwargs)
|
|
179
|
+
if elements:
|
|
180
|
+
return [StabilizedWebElement(el, self._engine) for el in elements]
|
|
181
|
+
|
|
182
|
+
# No elements found - wait and retry
|
|
183
|
+
time.sleep(poll_interval)
|
|
184
|
+
|
|
185
|
+
# Return empty list if nothing found
|
|
186
|
+
return []
|
|
127
187
|
|
|
128
188
|
@property
|
|
129
189
|
def unwrapped(self) -> 'WebDriver':
|
|
@@ -165,10 +225,25 @@ class SeleniumIntegration:
|
|
|
165
225
|
Returns:
|
|
166
226
|
StabilizedWebDriver that auto-waits before interactions
|
|
167
227
|
|
|
228
|
+
Raises:
|
|
229
|
+
TypeError: If driver is not a valid WebDriver instance
|
|
230
|
+
|
|
168
231
|
Note:
|
|
169
232
|
The returned driver wraps the original but is not a true WebDriver.
|
|
170
233
|
If you need the original for framework integration, use .unwrapped
|
|
171
234
|
"""
|
|
235
|
+
# Validate driver is a WebDriver-like object
|
|
236
|
+
if driver is None:
|
|
237
|
+
raise TypeError("driver cannot be None")
|
|
238
|
+
|
|
239
|
+
required_attrs = ['execute_script', 'find_element', 'current_url']
|
|
240
|
+
missing = [attr for attr in required_attrs if not hasattr(driver, attr)]
|
|
241
|
+
if missing:
|
|
242
|
+
raise TypeError(
|
|
243
|
+
f"driver does not appear to be a valid WebDriver. "
|
|
244
|
+
f"Missing required attributes: {missing}"
|
|
245
|
+
)
|
|
246
|
+
|
|
172
247
|
driver_id = id(driver)
|
|
173
248
|
|
|
174
249
|
if driver_id in self._wrapped_drivers:
|
|
@@ -154,11 +154,34 @@ class SignalEvaluator:
|
|
|
154
154
|
)
|
|
155
155
|
|
|
156
156
|
def _evaluate_dom(self, state: Dict[str, Any], current_time: float) -> Signal:
|
|
157
|
-
"""
|
|
157
|
+
"""
|
|
158
|
+
Evaluate DOM mutation activity using MUTATION RATE.
|
|
159
|
+
|
|
160
|
+
Key insight: Animated sites have steady ~30-50 mutations/sec (typewriter, particles).
|
|
161
|
+
Loading bursts have 100+ mutations/sec. We consider stable when rate is LOW, not zero.
|
|
162
|
+
|
|
163
|
+
Primary check: mutation_rate <= threshold (50/sec default)
|
|
164
|
+
Fallback: time since last mutation (for older browsers)
|
|
165
|
+
"""
|
|
166
|
+
mutation_rate = state.get('mutation_rate')
|
|
158
167
|
last_mutation = state.get('last_mutation_time', 0)
|
|
159
|
-
time_since_mutation = (current_time * 1000) - last_mutation # Convert to ms
|
|
160
|
-
threshold_ms = self.config.dom_settle_time * 1000
|
|
161
168
|
|
|
169
|
+
# Primary: Use mutation rate if available
|
|
170
|
+
if mutation_rate is not None:
|
|
171
|
+
threshold = self.config.mutation_rate_threshold
|
|
172
|
+
is_stable = mutation_rate <= threshold
|
|
173
|
+
return Signal(
|
|
174
|
+
signal_type=SignalType.DOM_MUTATIONS,
|
|
175
|
+
state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
|
|
176
|
+
value=mutation_rate,
|
|
177
|
+
threshold=threshold,
|
|
178
|
+
is_mandatory=True,
|
|
179
|
+
details=f"Mutation rate: {mutation_rate:.0f}/sec (threshold: {threshold:.0f}/sec)",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Fallback: Use time since last mutation
|
|
183
|
+
time_since_mutation = (current_time * 1000) - last_mutation
|
|
184
|
+
threshold_ms = self.config.dom_settle_time * 1000
|
|
162
185
|
is_stable = time_since_mutation >= threshold_ms
|
|
163
186
|
|
|
164
187
|
return Signal(
|
|
@@ -187,9 +210,15 @@ class SignalEvaluator:
|
|
|
187
210
|
)
|
|
188
211
|
|
|
189
212
|
def _evaluate_animations(self, state: Dict[str, Any]) -> Signal:
|
|
190
|
-
"""
|
|
213
|
+
"""
|
|
214
|
+
Evaluate CSS animation/transition activity.
|
|
215
|
+
|
|
216
|
+
Only mandatory in 'strict' mode. In 'normal' and 'relaxed' modes,
|
|
217
|
+
animations are cosmetic and don't block interaction.
|
|
218
|
+
"""
|
|
191
219
|
active = state.get('active_animations', 0)
|
|
192
|
-
|
|
220
|
+
# Only mandatory in strict mode - animations are usually cosmetic
|
|
221
|
+
is_mandatory = self.config.strictness == 'strict'
|
|
193
222
|
is_stable = active == 0
|
|
194
223
|
|
|
195
224
|
return Signal(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Eliminate explicit waits in UI automation by detecting true UI stability
|
|
5
|
-
Author-email: Dhiraj Das <dhirajdas.
|
|
5
|
+
Author-email: Dhiraj Das <dhirajdas.666@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://www.dhirajdas.dev
|
|
8
8
|
Project-URL: Documentation, https://github.com/godhiraj-code/waitless#readme
|
|
9
9
|
Project-URL: Repository, https://github.com/godhiraj-code/waitless.git
|
|
10
10
|
Project-URL: Issues, https://github.com/godhiraj-code/waitless/issues
|
|
@@ -22,12 +22,16 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
28
|
Requires-Dist: selenium>=4.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
28
30
|
|
|
29
31
|
# Waitless
|
|
30
32
|
|
|
33
|
+
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
34
|
+
|
|
31
35
|
**Zero-wait UI automation stabilization for Selenium**
|
|
32
36
|
|
|
33
37
|
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
@@ -93,12 +97,12 @@ When you interact, waitless ensures the page is truly ready.
|
|
|
93
97
|
from waitless import stabilize, StabilizationConfig
|
|
94
98
|
|
|
95
99
|
config = StabilizationConfig(
|
|
96
|
-
timeout=
|
|
97
|
-
|
|
98
|
-
network_idle_threshold=
|
|
99
|
-
animation_detection=True,
|
|
100
|
-
strictness='normal',
|
|
101
|
-
debug_mode=True
|
|
100
|
+
timeout=10, # Max wait time (seconds)
|
|
101
|
+
mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
|
|
102
|
+
network_idle_threshold=2, # Max pending requests (allows background traffic)
|
|
103
|
+
animation_detection=True, # Track CSS animations (non-blocking in normal mode)
|
|
104
|
+
strictness='normal', # 'strict' | 'normal' | 'relaxed'
|
|
105
|
+
debug_mode=True # Enable logging
|
|
102
106
|
)
|
|
103
107
|
|
|
104
108
|
driver = stabilize(driver, config=config)
|
|
@@ -185,7 +189,7 @@ Sample output:
|
|
|
185
189
|
|
|
186
190
|
### Network Threshold Warning
|
|
187
191
|
|
|
188
|
-
The default `network_idle_threshold=
|
|
192
|
+
The default `network_idle_threshold=2` allows some background traffic.
|
|
189
193
|
|
|
190
194
|
Many apps have background traffic that never stops:
|
|
191
195
|
- Analytics calls
|
|
@@ -210,7 +214,7 @@ element = driver.find_element(By.ID, "button")
|
|
|
210
214
|
original = element.unwrap() # Gets the real WebElement
|
|
211
215
|
```
|
|
212
216
|
|
|
213
|
-
## v0 Limitations
|
|
217
|
+
## v0.3 Limitations
|
|
214
218
|
|
|
215
219
|
- **Selenium only** - Playwright support planned for v1
|
|
216
220
|
- **Sync only** - No async/await support yet
|
|
@@ -218,6 +222,8 @@ original = element.unwrap() # Gets the real WebElement
|
|
|
218
222
|
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
219
223
|
- **No Service Workers** - SW network requests not intercepted
|
|
220
224
|
|
|
225
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
226
|
+
|
|
221
227
|
## API Reference
|
|
222
228
|
|
|
223
229
|
### Functions
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|