waitless 0.2.0__tar.gz → 0.3.1__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.2.0 → waitless-0.3.1}/LICENSE +1 -1
- {waitless-0.2.0/waitless.egg-info → waitless-0.3.1}/PKG-INFO +10 -7
- {waitless-0.2.0 → waitless-0.3.1}/README.md +7 -4
- {waitless-0.2.0 → waitless-0.3.1}/pyproject.toml +3 -3
- {waitless-0.2.0 → waitless-0.3.1}/waitless/__init__.py +1 -1
- {waitless-0.2.0 → waitless-0.3.1}/waitless/engine.py +19 -2
- {waitless-0.2.0 → waitless-0.3.1}/waitless/instrumentation.py +83 -4
- {waitless-0.2.0 → waitless-0.3.1}/waitless/selenium_integration.py +15 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless/signals.py +14 -2
- {waitless-0.2.0 → waitless-0.3.1/waitless.egg-info}/PKG-INFO +10 -7
- {waitless-0.2.0 → waitless-0.3.1}/setup.cfg +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless/__main__.py +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless/config.py +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless/diagnostics.py +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless/exceptions.py +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless.egg-info/SOURCES.txt +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless.egg-info/dependency_links.txt +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless.egg-info/entry_points.txt +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless.egg-info/requires.txt +0 -0
- {waitless-0.2.0 → waitless-0.3.1}/waitless.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -30,6 +30,8 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
# Waitless
|
|
32
32
|
|
|
33
|
+
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
34
|
+
|
|
33
35
|
**Zero-wait UI automation stabilization for Selenium**
|
|
34
36
|
|
|
35
37
|
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
@@ -82,7 +84,7 @@ Automation tests fail because interactions happen while the UI is still changing
|
|
|
82
84
|
|
|
83
85
|
Waitless monitors the **entire page** for stability signals:
|
|
84
86
|
|
|
85
|
-
- ✅ DOM mutation activity (MutationObserver)
|
|
87
|
+
- ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
|
|
86
88
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
87
89
|
- ✅ CSS animations and transitions
|
|
88
90
|
- ✅ Layout stability (element movement)
|
|
@@ -187,7 +189,7 @@ Sample output:
|
|
|
187
189
|
|
|
188
190
|
### Network Threshold Warning
|
|
189
191
|
|
|
190
|
-
The default `network_idle_threshold=
|
|
192
|
+
The default `network_idle_threshold=2` allows some background traffic.
|
|
191
193
|
|
|
192
194
|
Many apps have background traffic that never stops:
|
|
193
195
|
- Analytics calls
|
|
@@ -212,14 +214,15 @@ element = driver.find_element(By.ID, "button")
|
|
|
212
214
|
original = element.unwrap() # Gets the real WebElement
|
|
213
215
|
```
|
|
214
216
|
|
|
215
|
-
## v0 Limitations
|
|
217
|
+
## v0.3 Limitations
|
|
216
218
|
|
|
217
219
|
- **Selenium only** - Playwright support planned for v1
|
|
218
220
|
- **Sync only** - No async/await support yet
|
|
219
221
|
- **Main frame only** - iframes not monitored
|
|
220
|
-
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
221
222
|
- **No Service Workers** - SW network requests not intercepted
|
|
222
223
|
|
|
224
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
225
|
+
|
|
223
226
|
## API Reference
|
|
224
227
|
|
|
225
228
|
### 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.
|
|
@@ -52,7 +54,7 @@ Automation tests fail because interactions happen while the UI is still changing
|
|
|
52
54
|
|
|
53
55
|
Waitless monitors the **entire page** for stability signals:
|
|
54
56
|
|
|
55
|
-
- ✅ DOM mutation activity (MutationObserver)
|
|
57
|
+
- ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
|
|
56
58
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
57
59
|
- ✅ CSS animations and transitions
|
|
58
60
|
- ✅ Layout stability (element movement)
|
|
@@ -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,14 +184,15 @@ 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
|
|
189
191
|
- **Main frame only** - iframes not monitored
|
|
190
|
-
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
191
192
|
- **No Service Workers** - SW network requests not intercepted
|
|
192
193
|
|
|
194
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
195
|
+
|
|
193
196
|
## API Reference
|
|
194
197
|
|
|
195
198
|
### 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.1"
|
|
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"
|
|
@@ -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 (
|
|
@@ -86,7 +101,8 @@ class StabilizationEngine:
|
|
|
86
101
|
"""Get current page URL safely."""
|
|
87
102
|
try:
|
|
88
103
|
return self.driver.current_url
|
|
89
|
-
except
|
|
104
|
+
except (WebDriverException, NoSuchWindowException) as e:
|
|
105
|
+
self._debug(f"Could not get current URL: {e}")
|
|
90
106
|
return ""
|
|
91
107
|
|
|
92
108
|
def _is_instrumentation_alive(self) -> bool:
|
|
@@ -99,7 +115,8 @@ class StabilizationEngine:
|
|
|
99
115
|
try:
|
|
100
116
|
result = self.driver.execute_script(CHECK_ALIVE_SCRIPT)
|
|
101
117
|
return result is True
|
|
102
|
-
except
|
|
118
|
+
except (JavascriptException, WebDriverException, NoSuchWindowException) as e:
|
|
119
|
+
self._debug(f"Instrumentation check failed: {e}")
|
|
103
120
|
return False
|
|
104
121
|
|
|
105
122
|
def _inject_instrumentation(self) -> None:
|
|
@@ -71,6 +71,7 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
71
71
|
// Rolling window for mutation rate calculation
|
|
72
72
|
_mutationTimestamps: [],
|
|
73
73
|
_mutationWindowMs: 1000, // 1 second window for rate calculation
|
|
74
|
+
_observedShadowRoots: new WeakSet(),
|
|
74
75
|
|
|
75
76
|
_setupMutationObserver: function() {
|
|
76
77
|
var self = this;
|
|
@@ -87,17 +88,77 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
87
88
|
self._mutationTimestamps.shift();
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// Check for new shadow roots in added nodes
|
|
92
|
+
mutations.forEach(function(mutation) {
|
|
93
|
+
mutation.addedNodes.forEach(function(node) {
|
|
94
|
+
if (node.nodeType === 1) { // Element node
|
|
95
|
+
self._observeShadowRoots(node);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
90
100
|
self._log('DOM mutation', { count: mutations.length, rate: self.getMutationRate() });
|
|
91
101
|
});
|
|
92
102
|
|
|
93
|
-
|
|
103
|
+
var config = {
|
|
94
104
|
childList: true,
|
|
95
105
|
subtree: true,
|
|
96
106
|
attributes: true,
|
|
97
107
|
characterData: true
|
|
98
|
-
}
|
|
108
|
+
};
|
|
99
109
|
|
|
110
|
+
observer.observe(document.documentElement || document.body, config);
|
|
100
111
|
this._observers.push(observer);
|
|
112
|
+
|
|
113
|
+
// Initial scan for shadow roots
|
|
114
|
+
this._observeShadowRoots(document);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
_observeShadowRoots: function(root) {
|
|
118
|
+
var self = this;
|
|
119
|
+
|
|
120
|
+
// Function to recursively find and observe shadow roots
|
|
121
|
+
var walk = function(node) {
|
|
122
|
+
if (node.shadowRoot && !self._observedShadowRoots.has(node.shadowRoot)) {
|
|
123
|
+
self._observedShadowRoots.add(node.shadowRoot);
|
|
124
|
+
|
|
125
|
+
var observer = new MutationObserver(function(mutations) {
|
|
126
|
+
var now = Date.now();
|
|
127
|
+
self.lastMutationTime = now;
|
|
128
|
+
self._mutationTimestamps.push(now);
|
|
129
|
+
self._log('Shadow DOM mutation', { count: mutations.length });
|
|
130
|
+
|
|
131
|
+
// Scan new nodes in shadow DOM for nested shadow roots
|
|
132
|
+
mutations.forEach(function(mutation) {
|
|
133
|
+
mutation.addedNodes.forEach(function(newNode) {
|
|
134
|
+
if (newNode.nodeType === 1) walk(newNode);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
observer.observe(node.shadowRoot, {
|
|
140
|
+
childList: true,
|
|
141
|
+
subtree: true,
|
|
142
|
+
attributes: true,
|
|
143
|
+
characterData: true
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
self._observers.push(observer);
|
|
147
|
+
self._log('Observing shadow root', { host: node.tagName });
|
|
148
|
+
|
|
149
|
+
// Recurse into the shadow root
|
|
150
|
+
walk(node.shadowRoot);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Traverse children
|
|
154
|
+
var child = node.firstElementChild;
|
|
155
|
+
while (child) {
|
|
156
|
+
walk(child);
|
|
157
|
+
child = child.nextElementSibling;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
walk(root);
|
|
101
162
|
},
|
|
102
163
|
|
|
103
164
|
// Calculate mutations per second from rolling window
|
|
@@ -239,8 +300,26 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
239
300
|
},
|
|
240
301
|
|
|
241
302
|
_checkLayoutStability: function() {
|
|
242
|
-
// Track key interactive elements
|
|
243
|
-
var elements =
|
|
303
|
+
// Track key interactive elements, including those in shadow DOM
|
|
304
|
+
var elements = [];
|
|
305
|
+
|
|
306
|
+
var collectElements = function(root) {
|
|
307
|
+
var found = root.querySelectorAll('button, a, input, [onclick], [role="button"]');
|
|
308
|
+
for (var i = 0; i < found.length; i++) {
|
|
309
|
+
elements.push(found[i]);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Recursively check shadow roots
|
|
313
|
+
var all = root.querySelectorAll('*');
|
|
314
|
+
for (var j = 0; j < all.length; j++) {
|
|
315
|
+
if (all[j].shadowRoot) {
|
|
316
|
+
collectElements(all[j].shadowRoot);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
collectElements(document);
|
|
322
|
+
|
|
244
323
|
var isShifting = false;
|
|
245
324
|
var self = this;
|
|
246
325
|
|
|
@@ -225,10 +225,25 @@ class SeleniumIntegration:
|
|
|
225
225
|
Returns:
|
|
226
226
|
StabilizedWebDriver that auto-waits before interactions
|
|
227
227
|
|
|
228
|
+
Raises:
|
|
229
|
+
TypeError: If driver is not a valid WebDriver instance
|
|
230
|
+
|
|
228
231
|
Note:
|
|
229
232
|
The returned driver wraps the original but is not a true WebDriver.
|
|
230
233
|
If you need the original for framework integration, use .unwrapped
|
|
231
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
|
+
|
|
232
247
|
driver_id = id(driver)
|
|
233
248
|
|
|
234
249
|
if driver_id in self._wrapped_drivers:
|
|
@@ -169,14 +169,26 @@ class SignalEvaluator:
|
|
|
169
169
|
# Primary: Use mutation rate if available
|
|
170
170
|
if mutation_rate is not None:
|
|
171
171
|
threshold = self.config.mutation_rate_threshold
|
|
172
|
-
|
|
172
|
+
rate_stable = mutation_rate <= threshold
|
|
173
|
+
|
|
174
|
+
# Also check settle time to ensure we are quiet AFTER a mutation
|
|
175
|
+
time_since_mutation = (current_time * 1000) - last_mutation
|
|
176
|
+
threshold_ms = self.config.dom_settle_time * 1000
|
|
177
|
+
settle_stable = time_since_mutation >= threshold_ms
|
|
178
|
+
|
|
179
|
+
is_stable = rate_stable and settle_stable
|
|
180
|
+
|
|
181
|
+
details = f"Mutation rate: {mutation_rate:.0f}/sec (threshold: {threshold:.0f}/sec)"
|
|
182
|
+
if not settle_stable:
|
|
183
|
+
details += f", but only {time_since_mutation:.0f}ms since last mutation (need {threshold_ms:.0f}ms)"
|
|
184
|
+
|
|
173
185
|
return Signal(
|
|
174
186
|
signal_type=SignalType.DOM_MUTATIONS,
|
|
175
187
|
state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
|
|
176
188
|
value=mutation_rate,
|
|
177
189
|
threshold=threshold,
|
|
178
190
|
is_mandatory=True,
|
|
179
|
-
details=
|
|
191
|
+
details=details,
|
|
180
192
|
)
|
|
181
193
|
|
|
182
194
|
# Fallback: Use time since last mutation
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -30,6 +30,8 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
# Waitless
|
|
32
32
|
|
|
33
|
+
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
34
|
+
|
|
33
35
|
**Zero-wait UI automation stabilization for Selenium**
|
|
34
36
|
|
|
35
37
|
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
@@ -82,7 +84,7 @@ Automation tests fail because interactions happen while the UI is still changing
|
|
|
82
84
|
|
|
83
85
|
Waitless monitors the **entire page** for stability signals:
|
|
84
86
|
|
|
85
|
-
- ✅ DOM mutation activity (MutationObserver)
|
|
87
|
+
- ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
|
|
86
88
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
87
89
|
- ✅ CSS animations and transitions
|
|
88
90
|
- ✅ Layout stability (element movement)
|
|
@@ -187,7 +189,7 @@ Sample output:
|
|
|
187
189
|
|
|
188
190
|
### Network Threshold Warning
|
|
189
191
|
|
|
190
|
-
The default `network_idle_threshold=
|
|
192
|
+
The default `network_idle_threshold=2` allows some background traffic.
|
|
191
193
|
|
|
192
194
|
Many apps have background traffic that never stops:
|
|
193
195
|
- Analytics calls
|
|
@@ -212,14 +214,15 @@ element = driver.find_element(By.ID, "button")
|
|
|
212
214
|
original = element.unwrap() # Gets the real WebElement
|
|
213
215
|
```
|
|
214
216
|
|
|
215
|
-
## v0 Limitations
|
|
217
|
+
## v0.3 Limitations
|
|
216
218
|
|
|
217
219
|
- **Selenium only** - Playwright support planned for v1
|
|
218
220
|
- **Sync only** - No async/await support yet
|
|
219
221
|
- **Main frame only** - iframes not monitored
|
|
220
|
-
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
221
222
|
- **No Service Workers** - SW network requests not intercepted
|
|
222
223
|
|
|
224
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
225
|
+
|
|
223
226
|
## API Reference
|
|
224
227
|
|
|
225
228
|
### 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
|
|
File without changes
|
|
File without changes
|