waitless 0.1.0__tar.gz → 0.2.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.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waitless
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Eliminate explicit waits in UI automation by detecting true UI stability
5
5
  Author-email: Dhiraj Das <dhirajdas.66@gmail.com>
6
6
  License: MIT
@@ -22,9 +22,11 @@ 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
 
@@ -93,12 +95,12 @@ When you interact, waitless ensures the page is truly ready.
93
95
  from waitless import stabilize, StabilizationConfig
94
96
 
95
97
  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
98
+ timeout=10, # Max wait time (seconds)
99
+ mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
100
+ network_idle_threshold=2, # Max pending requests (allows background traffic)
101
+ animation_detection=True, # Track CSS animations (non-blocking in normal mode)
102
+ strictness='normal', # 'strict' | 'normal' | 'relaxed'
103
+ debug_mode=True # Enable logging
102
104
  )
103
105
 
104
106
  driver = stabilize(driver, config=config)
@@ -65,12 +65,12 @@ When you interact, waitless ensures the page is truly ready.
65
65
  from waitless import stabilize, StabilizationConfig
66
66
 
67
67
  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
68
+ timeout=10, # Max wait time (seconds)
69
+ mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
70
+ network_idle_threshold=2, # Max pending requests (allows background traffic)
71
+ animation_detection=True, # Track CSS animations (non-blocking in normal mode)
72
+ strictness='normal', # 'strict' | 'normal' | 'relaxed'
73
+ debug_mode=True # Enable logging
74
74
  )
75
75
 
76
76
  driver = stabilize(driver, config=config)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "waitless"
7
- version = "0.1.0"
7
+ version = "0.2.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"}
@@ -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,
@@ -53,6 +53,7 @@ class StabilizationEngine:
53
53
 
54
54
 
55
55
  self._last_status: Optional[StabilityStatus] = None
56
+ self._last_browser_state: Optional[Dict[str, Any]] = None
56
57
  self._last_blocking_factors: Dict[str, Any] = {}
57
58
  self._timeline: list = []
58
59
 
@@ -175,6 +176,7 @@ class StabilizationEngine:
175
176
  status = self.evaluator.evaluate(browser_state, current_time)
176
177
  last_status = status
177
178
  self._last_status = status
179
+ self._last_browser_state = browser_state # Store for diagnostics
178
180
 
179
181
  if status.is_stable:
180
182
  self._debug(f"UI stable after {elapsed:.2f}s")
@@ -260,7 +262,7 @@ class StabilizationEngine:
260
262
  'network_idle_threshold': self.config.network_idle_threshold,
261
263
  'animation_detection': self.config.animation_detection,
262
264
  },
263
- 'last_status': self._last_status.to_dict() if self._last_status else None,
265
+ 'last_status': self._last_browser_state, # Raw browser state with mutation_rate, etc.
264
266
  'blocking_factors': self._last_blocking_factors,
265
267
  'timeline': self._timeline[-50:],
266
268
  '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':
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waitless
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Eliminate explicit waits in UI automation by detecting true UI stability
5
5
  Author-email: Dhiraj Das <dhirajdas.66@gmail.com>
6
6
  License: MIT
@@ -22,9 +22,11 @@ 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
 
@@ -93,12 +95,12 @@ When you interact, waitless ensures the page is truly ready.
93
95
  from waitless import stabilize, StabilizationConfig
94
96
 
95
97
  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
98
+ timeout=10, # Max wait time (seconds)
99
+ mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
100
+ network_idle_threshold=2, # Max pending requests (allows background traffic)
101
+ animation_detection=True, # Track CSS animations (non-blocking in normal mode)
102
+ strictness='normal', # 'strict' | 'normal' | 'relaxed'
103
+ debug_mode=True # Enable logging
102
104
  )
103
105
 
104
106
  driver = stabilize(driver, config=config)
@@ -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
File without changes