waitless 0.3.0__tar.gz → 0.3.2__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/waitless.egg-info → waitless-0.3.2}/PKG-INFO +15 -16
- {waitless-0.3.0 → waitless-0.3.2}/README.md +14 -15
- {waitless-0.3.0 → waitless-0.3.2}/pyproject.toml +1 -1
- {waitless-0.3.0 → waitless-0.3.2}/waitless/__init__.py +1 -1
- {waitless-0.3.0 → waitless-0.3.2}/waitless/__main__.py +28 -28
- {waitless-0.3.0 → waitless-0.3.2}/waitless/diagnostics.py +30 -30
- {waitless-0.3.0 → waitless-0.3.2}/waitless/instrumentation.py +83 -4
- {waitless-0.3.0 → waitless-0.3.2}/waitless/signals.py +4 -1
- {waitless-0.3.0 → waitless-0.3.2/waitless.egg-info}/PKG-INFO +15 -16
- {waitless-0.3.0 → waitless-0.3.2}/LICENSE +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/setup.cfg +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless/config.py +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless/engine.py +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless/exceptions.py +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless/selenium_integration.py +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless.egg-info/SOURCES.txt +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless.egg-info/dependency_links.txt +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless.egg-info/entry_points.txt +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless.egg-info/requires.txt +0 -0
- {waitless-0.3.0 → waitless-0.3.2}/waitless.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Eliminate explicit waits in UI automation by detecting true UI stability
|
|
5
5
|
Author-email: Dhiraj Das <dhirajdas.666@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -84,7 +84,7 @@ Automation tests fail because interactions happen while the UI is still changing
|
|
|
84
84
|
|
|
85
85
|
Waitless monitors the **entire page** for stability signals:
|
|
86
86
|
|
|
87
|
-
- ✅ DOM mutation activity (MutationObserver)
|
|
87
|
+
- ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
|
|
88
88
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
89
89
|
- ✅ CSS animations and transitions
|
|
90
90
|
- ✅ Layout stability (element movement)
|
|
@@ -171,18 +171,18 @@ python -m waitless doctor --file diagnostics.json
|
|
|
171
171
|
|
|
172
172
|
Sample output:
|
|
173
173
|
```
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
+--------------------------------------------------------------------+
|
|
175
|
+
| WAITLESS STABILITY REPORT |
|
|
176
|
+
+--------------------------------------------------------------------+
|
|
177
|
+
| BLOCKING FACTORS: |
|
|
178
|
+
| [!] NETWORK: 2 request(s) still pending |
|
|
179
|
+
| -> GET /api/users |
|
|
180
|
+
| [!] ANIMATIONS: 1 active animation(s) |
|
|
181
|
+
+--------------------------------------------------------------------+
|
|
182
|
+
| SUGGESTIONS: |
|
|
183
|
+
| 1. Set network_idle_threshold=2 for background traffic |
|
|
184
|
+
| 2. Use animation_detection=False for infinite spinners |
|
|
185
|
+
+--------------------------------------------------------------------+
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
## Important Notes
|
|
@@ -214,12 +214,11 @@ element = driver.find_element(By.ID, "button")
|
|
|
214
214
|
original = element.unwrap() # Gets the real WebElement
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
## v0.3 Limitations
|
|
217
|
+
## v0.3.2 Limitations
|
|
218
218
|
|
|
219
219
|
- **Selenium only** - Playwright support planned for v1
|
|
220
220
|
- **Sync only** - No async/await support yet
|
|
221
221
|
- **Main frame only** - iframes not monitored
|
|
222
|
-
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
223
222
|
- **No Service Workers** - SW network requests not intercepted
|
|
224
223
|
|
|
225
224
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -54,7 +54,7 @@ Automation tests fail because interactions happen while the UI is still changing
|
|
|
54
54
|
|
|
55
55
|
Waitless monitors the **entire page** for stability signals:
|
|
56
56
|
|
|
57
|
-
- ✅ DOM mutation activity (MutationObserver)
|
|
57
|
+
- ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
|
|
58
58
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
59
59
|
- ✅ CSS animations and transitions
|
|
60
60
|
- ✅ Layout stability (element movement)
|
|
@@ -141,18 +141,18 @@ python -m waitless doctor --file diagnostics.json
|
|
|
141
141
|
|
|
142
142
|
Sample output:
|
|
143
143
|
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
144
|
+
+--------------------------------------------------------------------+
|
|
145
|
+
| WAITLESS STABILITY REPORT |
|
|
146
|
+
+--------------------------------------------------------------------+
|
|
147
|
+
| BLOCKING FACTORS: |
|
|
148
|
+
| [!] NETWORK: 2 request(s) still pending |
|
|
149
|
+
| -> GET /api/users |
|
|
150
|
+
| [!] ANIMATIONS: 1 active animation(s) |
|
|
151
|
+
+--------------------------------------------------------------------+
|
|
152
|
+
| SUGGESTIONS: |
|
|
153
|
+
| 1. Set network_idle_threshold=2 for background traffic |
|
|
154
|
+
| 2. Use animation_detection=False for infinite spinners |
|
|
155
|
+
+--------------------------------------------------------------------+
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
## Important Notes
|
|
@@ -184,12 +184,11 @@ element = driver.find_element(By.ID, "button")
|
|
|
184
184
|
original = element.unwrap() # Gets the real WebElement
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
## v0.3 Limitations
|
|
187
|
+
## v0.3.2 Limitations
|
|
188
188
|
|
|
189
189
|
- **Selenium only** - Playwright support planned for v1
|
|
190
190
|
- **Sync only** - No async/await support yet
|
|
191
191
|
- **Main frame only** - iframes not monitored
|
|
192
|
-
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
193
192
|
- **No Service Workers** - SW network requests not intercepted
|
|
194
193
|
|
|
195
194
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -71,34 +71,34 @@ def run_doctor(args):
|
|
|
71
71
|
return 1
|
|
72
72
|
else:
|
|
73
73
|
# Show usage instructions
|
|
74
|
-
print("
|
|
75
|
-
print("
|
|
76
|
-
print("
|
|
77
|
-
print("
|
|
78
|
-
print("
|
|
79
|
-
print("
|
|
80
|
-
print("
|
|
81
|
-
print("
|
|
82
|
-
print("
|
|
83
|
-
print("
|
|
84
|
-
print("
|
|
85
|
-
print("
|
|
86
|
-
print("
|
|
87
|
-
print("
|
|
88
|
-
print("
|
|
89
|
-
print("
|
|
90
|
-
print("
|
|
91
|
-
print("
|
|
92
|
-
print("
|
|
93
|
-
print("
|
|
94
|
-
print("
|
|
95
|
-
print("
|
|
96
|
-
print("
|
|
97
|
-
print("
|
|
98
|
-
print("
|
|
99
|
-
print("
|
|
100
|
-
print("
|
|
101
|
-
print("
|
|
74
|
+
print("+-" + "-" * 66 + "-+")
|
|
75
|
+
print("|" + "WAITLESS DOCTOR".center(66) + "|")
|
|
76
|
+
print("+-" + "-" * 66 + "-+")
|
|
77
|
+
print("|".ljust(67) + "|")
|
|
78
|
+
print("| The doctor command analyzes stability diagnostics.".ljust(67) + "|")
|
|
79
|
+
print("|".ljust(67) + "|")
|
|
80
|
+
print("| USAGE OPTIONS:".ljust(67) + "|")
|
|
81
|
+
print("|".ljust(67) + "|")
|
|
82
|
+
print("| 1. From a diagnostic file:".ljust(67) + "|")
|
|
83
|
+
print("| waitless doctor --file diagnostics.json".ljust(67) + "|")
|
|
84
|
+
print("|".ljust(67) + "|")
|
|
85
|
+
print("| 2. In your test code, capture diagnostics on failure:".ljust(67) + "|")
|
|
86
|
+
print("|".ljust(67) + "|")
|
|
87
|
+
print("| from waitless import get_diagnostics".ljust(67) + "|")
|
|
88
|
+
print("| from waitless.diagnostics import print_report".ljust(67) + "|")
|
|
89
|
+
print("|".ljust(67) + "|")
|
|
90
|
+
print("| try:".ljust(67) + "|")
|
|
91
|
+
print("| driver.find_element(...).click()".ljust(67) + "|")
|
|
92
|
+
print("| except StabilizationTimeout as e:".ljust(67) + "|")
|
|
93
|
+
print("| print_report(engine) # Print diagnostic report".ljust(67) + "|")
|
|
94
|
+
print("|".ljust(67) + "|")
|
|
95
|
+
print("| 3. Export diagnostics for CI:".ljust(67) + "|")
|
|
96
|
+
print("|".ljust(67) + "|")
|
|
97
|
+
print("| diagnostics = get_diagnostics(driver)".ljust(67) + "|")
|
|
98
|
+
print("| with open('diag.json', 'w') as f:".ljust(67) + "|")
|
|
99
|
+
print("| json.dump(diagnostics, f)".ljust(67) + "|")
|
|
100
|
+
print("|".ljust(67) + "|")
|
|
101
|
+
print("+-" + "-" * 66 + "-+")
|
|
102
102
|
return 0
|
|
103
103
|
|
|
104
104
|
# Generate report
|
|
@@ -24,71 +24,71 @@ class DiagnosticReport:
|
|
|
24
24
|
def generate_text_report(self) -> str:
|
|
25
25
|
"""Generate a text-based diagnostic report."""
|
|
26
26
|
lines = []
|
|
27
|
-
lines.append("
|
|
28
|
-
lines.append("
|
|
29
|
-
lines.append("
|
|
30
|
-
lines.append(f"
|
|
27
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
28
|
+
lines.append("|" + "WAITLESS STABILITY REPORT".center(66) + "|")
|
|
29
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
30
|
+
lines.append(f"| Report generated at: {self.timestamp.strftime('%Y-%m-%d %H:%M:%S'):<43} |")
|
|
31
31
|
|
|
32
32
|
config = self.diagnostics.get('config', {})
|
|
33
|
-
lines.append("
|
|
34
|
-
lines.append("
|
|
35
|
-
lines.append(f"
|
|
36
|
-
lines.append(f"
|
|
37
|
-
lines.append(f"
|
|
38
|
-
lines.append(f"
|
|
33
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
34
|
+
lines.append("| CONFIGURATION:".ljust(67) + "|")
|
|
35
|
+
lines.append(f"| Timeout: {config.get('timeout', 'N/A')}s".ljust(67) + "|")
|
|
36
|
+
lines.append(f"| Strictness: {config.get('strictness', 'N/A')}".ljust(67) + "|")
|
|
37
|
+
lines.append(f"| Network threshold: {config.get('network_idle_threshold', 'N/A')} pending requests".ljust(67) + "|")
|
|
38
|
+
lines.append(f"| Animation detection: {config.get('animation_detection', 'N/A')}".ljust(67) + "|")
|
|
39
39
|
|
|
40
40
|
blocking = self.diagnostics.get('blocking_factors', {})
|
|
41
41
|
if blocking:
|
|
42
|
-
lines.append("
|
|
43
|
-
lines.append("
|
|
44
|
-
lines.append("
|
|
42
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
43
|
+
lines.append("| BLOCKING FACTORS:".ljust(67) + "|")
|
|
44
|
+
lines.append("|".ljust(67) + "|")
|
|
45
45
|
|
|
46
46
|
pending = blocking.get('pending_requests', 0)
|
|
47
47
|
if pending > 0:
|
|
48
|
-
lines.append("
|
|
48
|
+
lines.append("| [!] NETWORK: {} request(s) still pending".format(pending).ljust(67) + "|")
|
|
49
49
|
|
|
50
50
|
details = blocking.get('pending_request_details', [])
|
|
51
51
|
for req in details[:5]: # Show max 5
|
|
52
52
|
url = req.get('url', 'unknown')[:50]
|
|
53
53
|
started = req.get('startTime', 0)
|
|
54
|
-
lines.append(f"
|
|
54
|
+
lines.append(f"| -> {req.get('type', 'unknown').upper()} {url}".ljust(67) + "|")
|
|
55
55
|
|
|
56
56
|
if len(details) > 5:
|
|
57
|
-
lines.append(f"
|
|
58
|
-
lines.append("
|
|
57
|
+
lines.append(f"| ... and {len(details) - 5} more".ljust(67) + "|")
|
|
58
|
+
lines.append("|".ljust(67) + "|")
|
|
59
59
|
|
|
60
60
|
animations = blocking.get('active_animations', 0)
|
|
61
61
|
if animations > 0:
|
|
62
|
-
lines.append(f"
|
|
63
|
-
lines.append("
|
|
62
|
+
lines.append(f"| [!] ANIMATIONS: {animations} active animation(s)".ljust(67) + "|")
|
|
63
|
+
lines.append("|".ljust(67) + "|")
|
|
64
64
|
|
|
65
65
|
if blocking.get('layout_shifting'):
|
|
66
|
-
lines.append("
|
|
67
|
-
lines.append("
|
|
66
|
+
lines.append("| [!] LAYOUT: Elements are still moving".ljust(67) + "|")
|
|
67
|
+
lines.append("|".ljust(67) + "|")
|
|
68
68
|
|
|
69
69
|
status = self.diagnostics.get('last_status')
|
|
70
70
|
if status:
|
|
71
|
-
lines.append("
|
|
72
|
-
lines.append("
|
|
71
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
72
|
+
lines.append("| SIGNAL STATUS:".ljust(67) + "|")
|
|
73
73
|
|
|
74
74
|
for signal in status.get('signals', []):
|
|
75
|
-
state = "
|
|
75
|
+
state = "[OK]" if signal['state'] == 'STABLE' else "[WAIT]"
|
|
76
76
|
mandatory = "[M]" if signal['mandatory'] else "[O]"
|
|
77
|
-
line = f"
|
|
78
|
-
lines.append(line[:66].ljust(67) + "
|
|
77
|
+
line = f"| {state} {mandatory} {signal['type']}: {signal.get('details', 'N/A')}"
|
|
78
|
+
lines.append(line[:66].ljust(67) + "|")
|
|
79
79
|
|
|
80
80
|
timeline = self.diagnostics.get('timeline', [])
|
|
81
81
|
if timeline:
|
|
82
|
-
lines.append("
|
|
83
|
-
lines.append("
|
|
82
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
83
|
+
lines.append("| RECENT EVENTS (last 10):".ljust(67) + "|")
|
|
84
84
|
|
|
85
85
|
for entry in timeline[-10:]:
|
|
86
86
|
time_str = str(entry.get('time', ''))[-6:]
|
|
87
87
|
msg = entry.get('message', '')[:50]
|
|
88
|
-
lines.append(f"
|
|
88
|
+
lines.append(f"| [{time_str}] {msg}".ljust(67) + "|")
|
|
89
89
|
|
|
90
90
|
lines.extend(self._generate_suggestions())
|
|
91
|
-
lines.append("
|
|
91
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
92
92
|
|
|
93
93
|
return "\n".join(lines)
|
|
94
94
|
|
|
@@ -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
|
|
|
@@ -170,13 +170,16 @@ class SignalEvaluator:
|
|
|
170
170
|
if mutation_rate is not None:
|
|
171
171
|
threshold = self.config.mutation_rate_threshold
|
|
172
172
|
is_stable = mutation_rate <= threshold
|
|
173
|
+
|
|
174
|
+
details = f"Mutation rate: {mutation_rate:.0f}/sec (threshold: {threshold:.0f}/sec)"
|
|
175
|
+
|
|
173
176
|
return Signal(
|
|
174
177
|
signal_type=SignalType.DOM_MUTATIONS,
|
|
175
178
|
state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
|
|
176
179
|
value=mutation_rate,
|
|
177
180
|
threshold=threshold,
|
|
178
181
|
is_mandatory=True,
|
|
179
|
-
details=
|
|
182
|
+
details=details,
|
|
180
183
|
)
|
|
181
184
|
|
|
182
185
|
# Fallback: Use time since last mutation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Eliminate explicit waits in UI automation by detecting true UI stability
|
|
5
5
|
Author-email: Dhiraj Das <dhirajdas.666@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -84,7 +84,7 @@ Automation tests fail because interactions happen while the UI is still changing
|
|
|
84
84
|
|
|
85
85
|
Waitless monitors the **entire page** for stability signals:
|
|
86
86
|
|
|
87
|
-
- ✅ DOM mutation activity (MutationObserver)
|
|
87
|
+
- ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
|
|
88
88
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
89
89
|
- ✅ CSS animations and transitions
|
|
90
90
|
- ✅ Layout stability (element movement)
|
|
@@ -171,18 +171,18 @@ python -m waitless doctor --file diagnostics.json
|
|
|
171
171
|
|
|
172
172
|
Sample output:
|
|
173
173
|
```
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
+--------------------------------------------------------------------+
|
|
175
|
+
| WAITLESS STABILITY REPORT |
|
|
176
|
+
+--------------------------------------------------------------------+
|
|
177
|
+
| BLOCKING FACTORS: |
|
|
178
|
+
| [!] NETWORK: 2 request(s) still pending |
|
|
179
|
+
| -> GET /api/users |
|
|
180
|
+
| [!] ANIMATIONS: 1 active animation(s) |
|
|
181
|
+
+--------------------------------------------------------------------+
|
|
182
|
+
| SUGGESTIONS: |
|
|
183
|
+
| 1. Set network_idle_threshold=2 for background traffic |
|
|
184
|
+
| 2. Use animation_detection=False for infinite spinners |
|
|
185
|
+
+--------------------------------------------------------------------+
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
## Important Notes
|
|
@@ -214,12 +214,11 @@ element = driver.find_element(By.ID, "button")
|
|
|
214
214
|
original = element.unwrap() # Gets the real WebElement
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
## v0.3 Limitations
|
|
217
|
+
## v0.3.2 Limitations
|
|
218
218
|
|
|
219
219
|
- **Selenium only** - Playwright support planned for v1
|
|
220
220
|
- **Sync only** - No async/await support yet
|
|
221
221
|
- **Main frame only** - iframes not monitored
|
|
222
|
-
- **No Shadow DOM** - MutationObserver doesn't see shadow roots
|
|
223
222
|
- **No Service Workers** - SW network requests not intercepted
|
|
224
223
|
|
|
225
224
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
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
|
|
File without changes
|