waitless 0.1.0__py3-none-any.whl

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/signals.py ADDED
@@ -0,0 +1,215 @@
1
+ """
2
+ Stability signal definitions and combination logic.
3
+
4
+ Signals are measurable indicators of UI state. This module defines
5
+ what signals exist, how to interpret them, and how to combine them
6
+ into an overall stability decision.
7
+
8
+ Note: This module is designed to be a future extension point.
9
+ Users may eventually be able to define custom signals.
10
+ """
11
+
12
+ from dataclasses import dataclass
13
+ from enum import Enum, auto
14
+ from typing import Dict, Any, List, Optional
15
+ from .config import StabilizationConfig
16
+
17
+
18
+ class SignalType(Enum):
19
+ """Types of stability signals we monitor."""
20
+ DOM_MUTATIONS = auto()
21
+ NETWORK_REQUESTS = auto()
22
+ CSS_ANIMATIONS = auto()
23
+ CSS_TRANSITIONS = auto()
24
+ LAYOUT_SHIFT = auto()
25
+ RAF_ACTIVITY = auto()
26
+
27
+
28
+ class SignalState(Enum):
29
+ """State of an individual signal."""
30
+ STABLE = auto() # Signal indicates stability
31
+ UNSTABLE = auto() # Signal indicates instability
32
+ UNKNOWN = auto() # Signal not yet measured
33
+
34
+
35
+ @dataclass
36
+ class Signal:
37
+ """
38
+ Represents a single stability signal measurement.
39
+
40
+ Attributes:
41
+ signal_type: The type of signal
42
+ state: Current state (stable/unstable/unknown)
43
+ value: Raw measurement value
44
+ threshold: Threshold used for stability decision
45
+ is_mandatory: Whether this signal must be stable
46
+ details: Additional diagnostic information
47
+ """
48
+ signal_type: SignalType
49
+ state: SignalState
50
+ value: Any
51
+ threshold: Any
52
+ is_mandatory: bool
53
+ details: Optional[str] = None
54
+
55
+ @property
56
+ def is_stable(self) -> bool:
57
+ return self.state == SignalState.STABLE
58
+
59
+ @property
60
+ def is_blocking(self) -> bool:
61
+ """Returns True if this signal is blocking overall stability."""
62
+ return self.is_mandatory and not self.is_stable
63
+
64
+
65
+ @dataclass
66
+ class StabilityStatus:
67
+ """
68
+ Overall stability status combining all signals.
69
+
70
+ Attributes:
71
+ is_stable: Whether UI is considered stable
72
+ signals: Individual signal measurements
73
+ blocking_signals: Signals preventing stability
74
+ timestamp: When this status was measured
75
+ """
76
+ is_stable: bool
77
+ signals: List[Signal]
78
+ timestamp: float
79
+
80
+ @property
81
+ def blocking_signals(self) -> List[Signal]:
82
+ """Get list of signals blocking stability."""
83
+ return [s for s in self.signals if s.is_blocking]
84
+
85
+ def to_dict(self) -> Dict[str, Any]:
86
+ """Convert to dictionary for diagnostics."""
87
+ return {
88
+ 'is_stable': self.is_stable,
89
+ 'timestamp': self.timestamp,
90
+ 'signals': [
91
+ {
92
+ 'type': s.signal_type.name,
93
+ 'state': s.state.name,
94
+ 'value': s.value,
95
+ 'threshold': s.threshold,
96
+ 'mandatory': s.is_mandatory,
97
+ 'details': s.details,
98
+ }
99
+ for s in self.signals
100
+ ],
101
+ 'blocking': [s.signal_type.name for s in self.blocking_signals],
102
+ }
103
+
104
+
105
+ class SignalEvaluator:
106
+ """
107
+ Evaluates browser state against configured thresholds.
108
+
109
+ This class interprets raw browser measurements and converts them
110
+ into Signal objects with stability decisions.
111
+ """
112
+
113
+ def __init__(self, config: StabilizationConfig):
114
+ self.config = config
115
+
116
+ def evaluate(self, browser_state: Dict[str, Any], current_time: float) -> StabilityStatus:
117
+ """
118
+ Evaluate browser state and return stability status.
119
+
120
+ Args:
121
+ browser_state: Raw state from JavaScript instrumentation
122
+ current_time: Current timestamp for calculations
123
+
124
+ Returns:
125
+ StabilityStatus with all signal evaluations
126
+ """
127
+ signals = []
128
+
129
+ dom_signal = self._evaluate_dom(browser_state, current_time)
130
+ signals.append(dom_signal)
131
+
132
+ network_signal = self._evaluate_network(browser_state)
133
+ signals.append(network_signal)
134
+
135
+ if self.config.animation_detection:
136
+ animation_signal = self._evaluate_animations(browser_state)
137
+ signals.append(animation_signal)
138
+
139
+ if self.config.strictness == 'strict' and self.config.layout_stability:
140
+ layout_signal = self._evaluate_layout(browser_state)
141
+ signals.append(layout_signal)
142
+
143
+ is_stable = all(
144
+ s.is_stable for s in signals if s.is_mandatory
145
+ )
146
+
147
+ if self.config.strictness == 'strict' and is_stable:
148
+ is_stable = all(s.is_stable for s in signals)
149
+
150
+ return StabilityStatus(
151
+ is_stable=is_stable,
152
+ signals=signals,
153
+ timestamp=current_time,
154
+ )
155
+
156
+ def _evaluate_dom(self, state: Dict[str, Any], current_time: float) -> Signal:
157
+ """Evaluate DOM mutation activity."""
158
+ 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
+
162
+ is_stable = time_since_mutation >= threshold_ms
163
+
164
+ return Signal(
165
+ signal_type=SignalType.DOM_MUTATIONS,
166
+ state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
167
+ value=time_since_mutation,
168
+ threshold=threshold_ms,
169
+ is_mandatory=True,
170
+ details=f"Last mutation {time_since_mutation:.0f}ms ago (need {threshold_ms:.0f}ms quiet)",
171
+ )
172
+
173
+ def _evaluate_network(self, state: Dict[str, Any]) -> Signal:
174
+ """Evaluate pending network requests."""
175
+ pending = state.get('pending_requests', 0)
176
+ threshold = self.config.network_idle_threshold
177
+
178
+ is_stable = pending <= threshold
179
+
180
+ return Signal(
181
+ signal_type=SignalType.NETWORK_REQUESTS,
182
+ state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
183
+ value=pending,
184
+ threshold=threshold,
185
+ is_mandatory=True,
186
+ details=f"{pending} pending request(s) (threshold: {threshold})",
187
+ )
188
+
189
+ def _evaluate_animations(self, state: Dict[str, Any]) -> Signal:
190
+ """Evaluate CSS animation/transition activity."""
191
+ active = state.get('active_animations', 0)
192
+ is_mandatory = self.config.strictness != 'relaxed'
193
+ is_stable = active == 0
194
+
195
+ return Signal(
196
+ signal_type=SignalType.CSS_ANIMATIONS,
197
+ state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
198
+ value=active,
199
+ threshold=0,
200
+ is_mandatory=is_mandatory,
201
+ details=f"{active} active animation(s)",
202
+ )
203
+
204
+ def _evaluate_layout(self, state: Dict[str, Any]) -> Signal:
205
+ """Evaluate layout stability (element movement)."""
206
+ is_shifting = state.get('layout_shifting', False)
207
+
208
+ return Signal(
209
+ signal_type=SignalType.LAYOUT_SHIFT,
210
+ state=SignalState.STABLE if not is_shifting else SignalState.UNSTABLE,
211
+ value=is_shifting,
212
+ threshold=False,
213
+ is_mandatory=self.config.strictness == 'strict',
214
+ details="Layout shifting detected" if is_shifting else "Layout stable",
215
+ )
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: waitless
3
+ Version: 0.1.0
4
+ Summary: Eliminate explicit waits in UI automation by detecting true UI stability
5
+ Author-email: Dhiraj Das <dhirajdas.66@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/godhiraj-code/waitless
8
+ Project-URL: Documentation, https://github.com/godhiraj-code/waitless#readme
9
+ Project-URL: Repository, https://github.com/godhiraj-code/waitless.git
10
+ Project-URL: Issues, https://github.com/godhiraj-code/waitless/issues
11
+ Keywords: selenium,automation,testing,ui-testing,wait,stability,flaky-tests
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: selenium>=4.0; extra == "dev"
28
+
29
+ # Waitless
30
+
31
+ **Zero-wait UI automation stabilization for Selenium**
32
+
33
+ Eliminate explicit waits and sleeps by automatically detecting true UI stability.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install waitless
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ from selenium import webdriver
45
+ from selenium.webdriver.common.by import By
46
+ from waitless import stabilize
47
+
48
+ # Create driver as usual
49
+ driver = webdriver.Chrome()
50
+
51
+ # Enable automatic stabilization - ONE LINE
52
+ driver = stabilize(driver)
53
+
54
+ # All interactions now auto-wait for stability
55
+ driver.get("https://example.com")
56
+ driver.find_element(By.ID, "login-button").click() # ← Auto-waits!
57
+ driver.find_element(By.ID, "username").send_keys("user") # ← Auto-waits!
58
+ ```
59
+
60
+ ## Why Waitless?
61
+
62
+ ### The Problem
63
+
64
+ Automation tests fail because interactions happen while the UI is still changing:
65
+
66
+ - DOM mutations from React/Vue/Angular updates
67
+ - In-flight AJAX requests
68
+ - CSS animations and transitions
69
+ - Layout shifts from lazy-loaded content
70
+
71
+ ### Traditional Solutions (and why they fail)
72
+
73
+ | Approach | Problem |
74
+ |----------|---------|
75
+ | `time.sleep(2)` | Too slow, still fails sometimes |
76
+ | `WebDriverWait` | Only checks one element, misses page-wide state |
77
+ | Retries | Masks the real problem, adds flakiness |
78
+
79
+ ### The Waitless Solution
80
+
81
+ Waitless monitors the **entire page** for stability signals:
82
+
83
+ - ✅ DOM mutation activity (MutationObserver)
84
+ - ✅ Pending network requests (XHR/fetch interception)
85
+ - ✅ CSS animations and transitions
86
+ - ✅ Layout stability (element movement)
87
+
88
+ When you interact, waitless ensures the page is truly ready.
89
+
90
+ ## Configuration
91
+
92
+ ```python
93
+ from waitless import stabilize, StabilizationConfig
94
+
95
+ 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
102
+ )
103
+
104
+ driver = stabilize(driver, config=config)
105
+ ```
106
+
107
+ ### Strictness Levels
108
+
109
+ | Level | What It Waits For |
110
+ |-------|-------------------|
111
+ | `strict` | DOM + Network + Animations + Layout |
112
+ | `normal` | DOM + Network (default) |
113
+ | `relaxed` | DOM only |
114
+
115
+ ### Factory Methods
116
+
117
+ ```python
118
+ # For strict testing
119
+ config = StabilizationConfig.strict()
120
+
121
+ # For apps with background traffic
122
+ config = StabilizationConfig.relaxed()
123
+
124
+ # For CI environments
125
+ config = StabilizationConfig.ci()
126
+ ```
127
+
128
+ ## Manual Stabilization
129
+
130
+ If you don't want to wrap the driver:
131
+
132
+ ```python
133
+ from waitless import wait_for_stability
134
+
135
+ wait_for_stability(driver)
136
+ driver.find_element(By.ID, "button").click()
137
+ ```
138
+
139
+ ## Disabling Stabilization
140
+
141
+ ```python
142
+ from waitless import unstabilize
143
+
144
+ driver = unstabilize(driver) # Back to original behavior
145
+ ```
146
+
147
+ ## Diagnostics
148
+
149
+ When tests fail, get detailed analysis:
150
+
151
+ ```python
152
+ from waitless import get_diagnostics, StabilizationTimeout
153
+ from waitless.diagnostics import print_report
154
+
155
+ try:
156
+ driver.find_element(By.ID, "slow-button").click()
157
+ except StabilizationTimeout as e:
158
+ diagnostics = get_diagnostics(driver)
159
+ print_report(engine) # Print detailed report
160
+ ```
161
+
162
+ ### CLI Doctor Command
163
+
164
+ ```bash
165
+ python -m waitless doctor --file diagnostics.json
166
+ ```
167
+
168
+ Sample output:
169
+ ```
170
+ ╔══════════════════════════════════════════════════════════════════╗
171
+ ║ WAITLESS STABILITY REPORT ║
172
+ ╠══════════════════════════════════════════════════════════════════╣
173
+ ║ BLOCKING FACTORS: ║
174
+ ║ ⚠ NETWORK: 2 request(s) still pending ║
175
+ ║ → GET /api/users ║
176
+ ║ ⚠ ANIMATIONS: 1 active animation(s) ║
177
+ ╠══════════════════════════════════════════════════════════════════╣
178
+ ║ SUGGESTIONS: ║
179
+ ║ 1. Set network_idle_threshold=2 for background traffic ║
180
+ ║ 2. Use animation_detection=False for infinite spinners ║
181
+ ╚══════════════════════════════════════════════════════════════════╝
182
+ ```
183
+
184
+ ## Important Notes
185
+
186
+ ### Network Threshold Warning
187
+
188
+ The default `network_idle_threshold=0` means **all** network requests must complete.
189
+
190
+ Many apps have background traffic that never stops:
191
+ - Analytics calls
192
+ - Long polling
193
+ - Feature flags
194
+ - WebSocket heartbeats
195
+
196
+ If tests timeout frequently, try:
197
+ ```python
198
+ config = StabilizationConfig(network_idle_threshold=2)
199
+ ```
200
+
201
+ ### Wrapped Elements
202
+
203
+ The stabilized driver returns wrapped elements that auto-wait. They behave like WebElements but:
204
+
205
+ - `isinstance(element, WebElement)` returns `False`
206
+ - Use `.unwrap()` to get the original element if needed
207
+
208
+ ```python
209
+ element = driver.find_element(By.ID, "button")
210
+ original = element.unwrap() # Gets the real WebElement
211
+ ```
212
+
213
+ ## v0 Limitations
214
+
215
+ - **Selenium only** - Playwright support planned for v1
216
+ - **Sync only** - No async/await support yet
217
+ - **Main frame only** - iframes not monitored
218
+ - **No Shadow DOM** - MutationObserver doesn't see shadow roots
219
+ - **No Service Workers** - SW network requests not intercepted
220
+
221
+ ## API Reference
222
+
223
+ ### Functions
224
+
225
+ | Function | Description |
226
+ |----------|-------------|
227
+ | `stabilize(driver, config=None)` | Enable auto-stabilization |
228
+ | `unstabilize(driver)` | Disable and return original driver |
229
+ | `wait_for_stability(driver, timeout=None)` | Manual one-time wait |
230
+ | `get_diagnostics(driver)` | Get diagnostic data |
231
+
232
+ ### Classes
233
+
234
+ | Class | Description |
235
+ |-------|-------------|
236
+ | `StabilizationConfig` | Configuration options |
237
+ | `StabilizedWebDriver` | Wrapped driver with auto-wait |
238
+ | `StabilizedWebElement` | Wrapped element with auto-wait |
239
+ | `StabilizationTimeout` | Exception when UI doesn't stabilize |
240
+
241
+ ## License
242
+
243
+ MIT
@@ -0,0 +1,14 @@
1
+ waitless/__init__.py,sha256=R3RDZb6qVdCWPmhVbCXNU_VFHHuinV7YlB2pdK6SU1A,2244
2
+ waitless/__main__.py,sha256=hqJRbJOqOhGJIMBOL6vsvRFmk8KsAofuFAOg65P4KJo,3915
3
+ waitless/config.py,sha256=AJEUUXZwKvTkCl-jIjLZHfk93EE2sHeXn_bY7mN7HeA,6068
4
+ waitless/diagnostics.py,sha256=BmPhaXMhJpWh2fR7fPzM8f6_wnqu4Uxurq1_IhzMaRM,7709
5
+ waitless/engine.py,sha256=fvUY-64rQjPVabDL5oGj4MUpuBFF8j9m9Kbvzohc8yA,9854
6
+ waitless/exceptions.py,sha256=xSsOjNgpSrx7hXqg1xKFAq6quIa8VWx81YyZ5GOUllQ,3578
7
+ waitless/instrumentation.py,sha256=6XF4_xJnTx3mKNR5ySTeI7s6iIcq9t9qvuRl8SwrLa8,11606
8
+ waitless/selenium_integration.py,sha256=mUS_wRowkXQZuvMS0auHZ1MzTuWoqCU6wn5jOcJM-M0,10061
9
+ waitless/signals.py,sha256=lCgSNHhm1309gy2blQdjK8hTyZvXSYuFi9H0P6x0ngs,7537
10
+ waitless-0.1.0.dist-info/METADATA,sha256=o6TzoJfC_WFjWSynpB9yfTncylFMzNeEKjA8DMRn064,7925
11
+ waitless-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ waitless-0.1.0.dist-info/entry_points.txt,sha256=xbJnCccN-M60RRIAaLOV8ILP6C6xwt18ykmfYJXUShU,52
13
+ waitless-0.1.0.dist-info/top_level.txt,sha256=tRSHQHQXgHldCfnMRAjwrjd4ezBGe4dJUCOD5ABtPJc,9
14
+ waitless-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ waitless = waitless.__main__:main
@@ -0,0 +1 @@
1
+ waitless