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 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.1.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.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
@@ -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
+ [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](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=5, # Max wait time (seconds)
97
- dom_settle_time=0.1, # DOM quiet period needed
98
- network_idle_threshold=0, # Max pending requests (0 = all must complete)
99
- animation_detection=True, # Wait for animations to finish
100
- strictness='normal', # 'strict' | 'normal' | 'relaxed'
101
- debug_mode=True # Enable logging
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=0` means **all** network requests must complete.
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
+ [![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.
@@ -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=5, # Max wait time (seconds)
69
- dom_settle_time=0.1, # DOM quiet period needed
70
- network_idle_threshold=0, # Max pending requests (0 = all must complete)
71
- animation_detection=True, # Wait for animations to finish
72
- strictness='normal', # 'strict' | 'normal' | 'relaxed'
73
- debug_mode=True # Enable logging
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=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,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.1.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.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.0'
40
40
  __author__ = 'Dhiraj Das'
41
41
 
42
42
  # Public API
@@ -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 0 (all requests must complete).
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 (faster)
47
- - 'relaxed': DOM only (fastest, least reliable)
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
- network_idle_threshold: int = 0
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 Exception:
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 Exception:
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._last_status.to_dict() if self._last_status else None,
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
- self.lastMutationTime = Date.now();
75
- self._log('DOM mutation', { count: mutations.length });
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
- """Find element and wrap it for stabilization."""
120
- element = self._driver.find_element(*args, **kwargs)
121
- return StabilizedWebElement(element, self._engine)
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
- """Find elements and wrap them for stabilization."""
125
- elements = self._driver.find_elements(*args, **kwargs)
126
- return [StabilizedWebElement(el, self._engine) for el in elements]
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
- """Evaluate DOM mutation activity."""
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
- """Evaluate CSS animation/transition activity."""
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
- is_mandatory = self.config.strictness != 'relaxed'
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.1.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.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
@@ -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
+ [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](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=5, # Max wait time (seconds)
97
- dom_settle_time=0.1, # DOM quiet period needed
98
- network_idle_threshold=0, # Max pending requests (0 = all must complete)
99
- animation_detection=True, # Wait for animations to finish
100
- strictness='normal', # 'strict' | 'normal' | 'relaxed'
101
- debug_mode=True # Enable logging
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=0` means **all** network requests must complete.
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,3 +1,4 @@
1
+ LICENSE
1
2
  README.md
2
3
  pyproject.toml
3
4
  waitless/__init__.py
File without changes
File without changes