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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Dhiraj Das
3
+ Copyright (c) 2025 Dhiraj Das
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waitless
3
- Version: 0.2.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.66@gmail.com>
5
+ Author-email: Dhiraj Das <dhirajdas.666@gmail.com>
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/godhiraj-code/waitless
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
+ [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](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=0` means **all** network requests must complete.
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
+ [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](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=0` means **all** network requests must complete.
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.2.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.66@gmail.com"}
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://github.com/godhiraj-code/waitless"
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"
@@ -36,7 +36,7 @@ Disable:
36
36
  driver = unstabilize(driver) # Back to original behavior
37
37
  """
38
38
 
39
- __version__ = '0.1.0'
39
+ __version__ = '0.3.1'
40
40
  __author__ = 'Dhiraj Das'
41
41
 
42
42
  # Public API
@@ -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 Exception:
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 Exception:
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
- observer.observe(document.documentElement || document.body, {
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 = document.querySelectorAll('button, a, input, [onclick], [role="button"]');
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
- is_stable = mutation_rate <= threshold
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=f"Mutation rate: {mutation_rate:.0f}/sec (threshold: {threshold:.0f}/sec)",
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.2.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.66@gmail.com>
5
+ Author-email: Dhiraj Das <dhirajdas.666@gmail.com>
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/godhiraj-code/waitless
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
+ [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](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=0` means **all** network requests must complete.
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