waitless 0.3.1__tar.gz → 1.0.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.1/waitless.egg-info → waitless-1.0.0}/PKG-INFO +56 -16
- {waitless-0.3.1 → waitless-1.0.0}/README.md +55 -15
- {waitless-0.3.1 → waitless-1.0.0}/pyproject.toml +1 -1
- {waitless-0.3.1 → waitless-1.0.0}/waitless/__init__.py +1 -1
- {waitless-0.3.1 → waitless-1.0.0}/waitless/__main__.py +33 -28
- waitless-1.0.0/waitless/adapters/__init__.py +20 -0
- waitless-1.0.0/waitless/adapters/angular.py +109 -0
- waitless-1.0.0/waitless/adapters/base.py +73 -0
- waitless-1.0.0/waitless/adapters/react.py +111 -0
- waitless-1.0.0/waitless/adapters/vue.py +117 -0
- {waitless-0.3.1 → waitless-1.0.0}/waitless/config.py +27 -1
- {waitless-0.3.1 → waitless-1.0.0}/waitless/diagnostics.py +56 -30
- {waitless-0.3.1 → waitless-1.0.0}/waitless/instrumentation.py +223 -1
- {waitless-0.3.1 → waitless-1.0.0}/waitless/signals.py +62 -10
- {waitless-0.3.1 → waitless-1.0.0/waitless.egg-info}/PKG-INFO +56 -16
- {waitless-0.3.1 → waitless-1.0.0}/waitless.egg-info/SOURCES.txt +6 -1
- {waitless-0.3.1 → waitless-1.0.0}/LICENSE +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/setup.cfg +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/waitless/engine.py +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/waitless/exceptions.py +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/waitless/selenium_integration.py +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/waitless.egg-info/dependency_links.txt +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/waitless.egg-info/entry_points.txt +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/waitless.egg-info/requires.txt +0 -0
- {waitless-0.3.1 → waitless-1.0.0}/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
|
+
Version: 1.0.0
|
|
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
|
|
@@ -36,6 +36,8 @@ Dynamic: license-file
|
|
|
36
36
|
|
|
37
37
|
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
38
38
|
|
|
39
|
+
|
|
40
|
+
|
|
39
41
|
## Installation
|
|
40
42
|
|
|
41
43
|
```bash
|
|
@@ -88,6 +90,9 @@ Waitless monitors the **entire page** for stability signals:
|
|
|
88
90
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
89
91
|
- ✅ CSS animations and transitions
|
|
90
92
|
- ✅ Layout stability (element movement)
|
|
93
|
+
- ✅ WebSocket/SSE activity (opt-in)
|
|
94
|
+
- ✅ Framework hooks (React/Angular/Vue, opt-in)
|
|
95
|
+
- ✅ iframe monitoring (opt-in)
|
|
91
96
|
|
|
92
97
|
When you interact, waitless ensures the page is truly ready.
|
|
93
98
|
|
|
@@ -171,18 +176,18 @@ python -m waitless doctor --file diagnostics.json
|
|
|
171
176
|
|
|
172
177
|
Sample output:
|
|
173
178
|
```
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
+--------------------------------------------------------------------+
|
|
180
|
+
| WAITLESS STABILITY REPORT |
|
|
181
|
+
+--------------------------------------------------------------------+
|
|
182
|
+
| BLOCKING FACTORS: |
|
|
183
|
+
| [!] NETWORK: 2 request(s) still pending |
|
|
184
|
+
| -> GET /api/users |
|
|
185
|
+
| [!] ANIMATIONS: 1 active animation(s) |
|
|
186
|
+
+--------------------------------------------------------------------+
|
|
187
|
+
| SUGGESTIONS: |
|
|
188
|
+
| 1. Set network_idle_threshold=2 for background traffic |
|
|
189
|
+
| 2. Use animation_detection=False for infinite spinners |
|
|
190
|
+
+--------------------------------------------------------------------+
|
|
186
191
|
```
|
|
187
192
|
|
|
188
193
|
## Important Notes
|
|
@@ -214,11 +219,46 @@ element = driver.find_element(By.ID, "button")
|
|
|
214
219
|
original = element.unwrap() # Gets the real WebElement
|
|
215
220
|
```
|
|
216
221
|
|
|
217
|
-
##
|
|
222
|
+
## v1.0.0 New Features
|
|
223
|
+
|
|
224
|
+
- **WebSocket/SSE Awareness** - Track WebSocket and Server-Sent Events activity
|
|
225
|
+
- **Framework Adapters** - React, Angular, Vue hooks for framework-specific settling
|
|
226
|
+
- **iframe Support** - Monitor same-origin iframes
|
|
227
|
+
- **Performance Benchmarks** - Built-in benchmark suite
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
# Enable new v1.0 features
|
|
231
|
+
config = StabilizationConfig(
|
|
232
|
+
track_websocket=True, # WebSocket monitoring
|
|
233
|
+
track_sse=True, # SSE monitoring
|
|
234
|
+
framework_hooks=['react'], # React adapter
|
|
235
|
+
track_iframes=True, # iframe monitoring
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Performance
|
|
240
|
+
|
|
241
|
+
| Metric | Typical Value |
|
|
242
|
+
|--------|---------------|
|
|
243
|
+
| Instrumentation injection | ~5-10ms |
|
|
244
|
+
| Per-poll overhead | ~1-2ms |
|
|
245
|
+
| Poll interval (default) | 50ms |
|
|
246
|
+
| Typical stabilization | 50-200ms after activity |
|
|
247
|
+
|
|
248
|
+
### SPA Navigation Handling
|
|
249
|
+
|
|
250
|
+
Waitless automatically re-injects instrumentation after SPA route changes:
|
|
251
|
+
|
|
252
|
+
1. Checks `__waitless__.isAlive()` before each wait
|
|
253
|
+
2. Detects URL changes via `driver.current_url`
|
|
254
|
+
3. Re-injects if instrumentation is missing
|
|
255
|
+
|
|
256
|
+
This works transparently with React Router, Vue Router, Angular Router, etc.
|
|
257
|
+
|
|
258
|
+
## Current Limitations
|
|
218
259
|
|
|
219
|
-
- **Selenium only** - Playwright support planned
|
|
260
|
+
- **Selenium only** - Playwright support planned
|
|
220
261
|
- **Sync only** - No async/await support yet
|
|
221
|
-
- **Main frame only** - iframes not monitored
|
|
222
262
|
- **No Service Workers** - SW network requests not intercepted
|
|
223
263
|
|
|
224
264
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
11
13
|
```bash
|
|
@@ -58,6 +60,9 @@ Waitless monitors the **entire page** for stability signals:
|
|
|
58
60
|
- ✅ Pending network requests (XHR/fetch interception)
|
|
59
61
|
- ✅ CSS animations and transitions
|
|
60
62
|
- ✅ Layout stability (element movement)
|
|
63
|
+
- ✅ WebSocket/SSE activity (opt-in)
|
|
64
|
+
- ✅ Framework hooks (React/Angular/Vue, opt-in)
|
|
65
|
+
- ✅ iframe monitoring (opt-in)
|
|
61
66
|
|
|
62
67
|
When you interact, waitless ensures the page is truly ready.
|
|
63
68
|
|
|
@@ -141,18 +146,18 @@ python -m waitless doctor --file diagnostics.json
|
|
|
141
146
|
|
|
142
147
|
Sample output:
|
|
143
148
|
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
+--------------------------------------------------------------------+
|
|
150
|
+
| WAITLESS STABILITY REPORT |
|
|
151
|
+
+--------------------------------------------------------------------+
|
|
152
|
+
| BLOCKING FACTORS: |
|
|
153
|
+
| [!] NETWORK: 2 request(s) still pending |
|
|
154
|
+
| -> GET /api/users |
|
|
155
|
+
| [!] ANIMATIONS: 1 active animation(s) |
|
|
156
|
+
+--------------------------------------------------------------------+
|
|
157
|
+
| SUGGESTIONS: |
|
|
158
|
+
| 1. Set network_idle_threshold=2 for background traffic |
|
|
159
|
+
| 2. Use animation_detection=False for infinite spinners |
|
|
160
|
+
+--------------------------------------------------------------------+
|
|
156
161
|
```
|
|
157
162
|
|
|
158
163
|
## Important Notes
|
|
@@ -184,11 +189,46 @@ element = driver.find_element(By.ID, "button")
|
|
|
184
189
|
original = element.unwrap() # Gets the real WebElement
|
|
185
190
|
```
|
|
186
191
|
|
|
187
|
-
##
|
|
192
|
+
## v1.0.0 New Features
|
|
193
|
+
|
|
194
|
+
- **WebSocket/SSE Awareness** - Track WebSocket and Server-Sent Events activity
|
|
195
|
+
- **Framework Adapters** - React, Angular, Vue hooks for framework-specific settling
|
|
196
|
+
- **iframe Support** - Monitor same-origin iframes
|
|
197
|
+
- **Performance Benchmarks** - Built-in benchmark suite
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
# Enable new v1.0 features
|
|
201
|
+
config = StabilizationConfig(
|
|
202
|
+
track_websocket=True, # WebSocket monitoring
|
|
203
|
+
track_sse=True, # SSE monitoring
|
|
204
|
+
framework_hooks=['react'], # React adapter
|
|
205
|
+
track_iframes=True, # iframe monitoring
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Performance
|
|
210
|
+
|
|
211
|
+
| Metric | Typical Value |
|
|
212
|
+
|--------|---------------|
|
|
213
|
+
| Instrumentation injection | ~5-10ms |
|
|
214
|
+
| Per-poll overhead | ~1-2ms |
|
|
215
|
+
| Poll interval (default) | 50ms |
|
|
216
|
+
| Typical stabilization | 50-200ms after activity |
|
|
217
|
+
|
|
218
|
+
### SPA Navigation Handling
|
|
219
|
+
|
|
220
|
+
Waitless automatically re-injects instrumentation after SPA route changes:
|
|
221
|
+
|
|
222
|
+
1. Checks `__waitless__.isAlive()` before each wait
|
|
223
|
+
2. Detects URL changes via `driver.current_url`
|
|
224
|
+
3. Re-injects if instrumentation is missing
|
|
225
|
+
|
|
226
|
+
This works transparently with React Router, Vue Router, Angular Router, etc.
|
|
227
|
+
|
|
228
|
+
## Current Limitations
|
|
188
229
|
|
|
189
|
-
- **Selenium only** - Playwright support planned
|
|
230
|
+
- **Selenium only** - Playwright support planned
|
|
190
231
|
- **Sync only** - No async/await support yet
|
|
191
|
-
- **Main frame only** - iframes not monitored
|
|
192
232
|
- **No Service Workers** - SW network requests not intercepted
|
|
193
233
|
|
|
194
234
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -71,34 +71,39 @@ 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 v1.0".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("| v1.0 FEATURES:".ljust(67) + "|")
|
|
81
|
+
print("| - WebSocket/SSE tracking (track_websocket, track_sse)".ljust(67) + "|")
|
|
82
|
+
print("| - Framework adapters (framework_hooks=['react','angular','vue'])".ljust(67) + "|")
|
|
83
|
+
print("| - iframe monitoring (track_iframes)".ljust(67) + "|")
|
|
84
|
+
print("|".ljust(67) + "|")
|
|
85
|
+
print("| USAGE OPTIONS:".ljust(67) + "|")
|
|
86
|
+
print("|".ljust(67) + "|")
|
|
87
|
+
print("| 1. From a diagnostic file:".ljust(67) + "|")
|
|
88
|
+
print("| waitless doctor --file diagnostics.json".ljust(67) + "|")
|
|
89
|
+
print("|".ljust(67) + "|")
|
|
90
|
+
print("| 2. In your test code, capture diagnostics on failure:".ljust(67) + "|")
|
|
91
|
+
print("|".ljust(67) + "|")
|
|
92
|
+
print("| from waitless import get_diagnostics".ljust(67) + "|")
|
|
93
|
+
print("| from waitless.diagnostics import print_report".ljust(67) + "|")
|
|
94
|
+
print("|".ljust(67) + "|")
|
|
95
|
+
print("| try:".ljust(67) + "|")
|
|
96
|
+
print("| driver.find_element(...).click()".ljust(67) + "|")
|
|
97
|
+
print("| except StabilizationTimeout as e:".ljust(67) + "|")
|
|
98
|
+
print("| print_report(engine) # Print diagnostic report".ljust(67) + "|")
|
|
99
|
+
print("|".ljust(67) + "|")
|
|
100
|
+
print("| 3. Export diagnostics for CI:".ljust(67) + "|")
|
|
101
|
+
print("|".ljust(67) + "|")
|
|
102
|
+
print("| diagnostics = get_diagnostics(driver)".ljust(67) + "|")
|
|
103
|
+
print("| with open('diag.json', 'w') as f:".ljust(67) + "|")
|
|
104
|
+
print("| json.dump(diagnostics, f)".ljust(67) + "|")
|
|
105
|
+
print("|".ljust(67) + "|")
|
|
106
|
+
print("+-" + "-" * 66 + "-+")
|
|
102
107
|
return 0
|
|
103
108
|
|
|
104
109
|
# Generate report
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Waitless Framework Adapters
|
|
3
|
+
|
|
4
|
+
This module provides optional framework-specific hooks for detecting
|
|
5
|
+
when React, Angular, or Vue have finished settling their internal work.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import FrameworkAdapter, get_adapter, get_available_adapters
|
|
9
|
+
from .react import ReactAdapter
|
|
10
|
+
from .angular import AngularAdapter
|
|
11
|
+
from .vue import VueAdapter
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'FrameworkAdapter',
|
|
15
|
+
'get_adapter',
|
|
16
|
+
'get_available_adapters',
|
|
17
|
+
'ReactAdapter',
|
|
18
|
+
'AngularAdapter',
|
|
19
|
+
'VueAdapter',
|
|
20
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Angular framework adapter for detecting Angular-specific settling work.
|
|
3
|
+
|
|
4
|
+
Detects Angular by looking for ng-version attribute or NgZone.
|
|
5
|
+
Monitors NgZone stability to detect when Angular has finished its work.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import FrameworkAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AngularAdapter(FrameworkAdapter):
|
|
12
|
+
"""
|
|
13
|
+
Adapter for Angular framework.
|
|
14
|
+
|
|
15
|
+
Hooks into Angular's NgZone to detect when all async tasks have completed
|
|
16
|
+
and Angular is in a stable state.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return 'angular'
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def detection_script(self) -> str:
|
|
25
|
+
return """
|
|
26
|
+
(function() {
|
|
27
|
+
// Check for Angular version attribute
|
|
28
|
+
if (document.querySelector('[ng-version]')) return true;
|
|
29
|
+
|
|
30
|
+
// Check for Angular global (older versions)
|
|
31
|
+
if (window.ng) return true;
|
|
32
|
+
|
|
33
|
+
// Check for getAllAngularRootElements
|
|
34
|
+
if (window.getAllAngularRootElements) return true;
|
|
35
|
+
|
|
36
|
+
return false;
|
|
37
|
+
})();
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def instrumentation_script(self) -> str:
|
|
42
|
+
return """
|
|
43
|
+
(function() {
|
|
44
|
+
if (!window.__waitless__) return false;
|
|
45
|
+
if (window.__waitless__._angularHooked) return true;
|
|
46
|
+
|
|
47
|
+
window.__waitless__.framework = window.__waitless__.framework || {};
|
|
48
|
+
window.__waitless__.framework.angular = {
|
|
49
|
+
isStable: true,
|
|
50
|
+
lastStableTime: Date.now(),
|
|
51
|
+
pendingTasks: 0
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Try to get NgZone from Angular's testability API
|
|
55
|
+
var testability = window.getAllAngularTestabilities && window.getAllAngularTestabilities();
|
|
56
|
+
if (testability && testability.length > 0) {
|
|
57
|
+
testability.forEach(function(t) {
|
|
58
|
+
t.whenStable(function() {
|
|
59
|
+
window.__waitless__.framework.angular.isStable = true;
|
|
60
|
+
window.__waitless__.framework.angular.lastStableTime = Date.now();
|
|
61
|
+
window.__waitless__._log('Angular became stable');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Also try to hook into Zone.js if available
|
|
67
|
+
if (window.Zone && window.Zone.current) {
|
|
68
|
+
var originalRun = Zone.prototype.run;
|
|
69
|
+
Zone.prototype.run = function(callback, applyThis, applyArgs) {
|
|
70
|
+
if (this.name === 'angular') {
|
|
71
|
+
window.__waitless__.framework.angular.isStable = false;
|
|
72
|
+
window.__waitless__.framework.angular.pendingTasks++;
|
|
73
|
+
}
|
|
74
|
+
var result = originalRun.apply(this, arguments);
|
|
75
|
+
if (this.name === 'angular') {
|
|
76
|
+
window.__waitless__.framework.angular.pendingTasks--;
|
|
77
|
+
if (window.__waitless__.framework.angular.pendingTasks === 0) {
|
|
78
|
+
window.__waitless__.framework.angular.isStable = true;
|
|
79
|
+
window.__waitless__.framework.angular.lastStableTime = Date.now();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
window.__waitless__._angularHooked = true;
|
|
87
|
+
window.__waitless__._log('Angular adapter installed');
|
|
88
|
+
return true;
|
|
89
|
+
})();
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def get_status_script(self) -> str:
|
|
93
|
+
return """
|
|
94
|
+
(function() {
|
|
95
|
+
if (!window.__waitless__ || !window.__waitless__.framework || !window.__waitless__.framework.angular) {
|
|
96
|
+
return { stable: true, details: 'Angular not detected' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var angular = window.__waitless__.framework.angular;
|
|
100
|
+
var timeSinceStable = Date.now() - angular.lastStableTime;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
stable: angular.isStable,
|
|
104
|
+
details: angular.isStable
|
|
105
|
+
? 'NgZone stable, last activity ' + timeSinceStable + 'ms ago'
|
|
106
|
+
: 'NgZone unstable, pending=' + angular.pendingTasks
|
|
107
|
+
};
|
|
108
|
+
})();
|
|
109
|
+
"""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base adapter interface for framework-specific hooks.
|
|
3
|
+
|
|
4
|
+
Adapters detect when a framework has finished its internal work
|
|
5
|
+
(rendering, effects, zone tasks) beyond what DOM observation captures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FrameworkAdapter(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Base class for framework-specific stability detection.
|
|
15
|
+
|
|
16
|
+
Subclasses implement framework-specific JavaScript that detects
|
|
17
|
+
when the framework has finished its internal processing.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
"""Unique identifier for this adapter (e.g., 'react', 'angular')."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def detection_script(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
JavaScript that detects if this framework is present on the page.
|
|
31
|
+
Should return true if the framework is detected.
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def instrumentation_script(self) -> str:
|
|
38
|
+
"""
|
|
39
|
+
JavaScript to inject for monitoring framework activity.
|
|
40
|
+
Should set window.__waitless__.framework[name] with status info.
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_status_script(self) -> str:
|
|
46
|
+
"""
|
|
47
|
+
JavaScript that returns the current framework stability status.
|
|
48
|
+
Should return { stable: bool, details: string }.
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_adapter(name: str) -> Optional['FrameworkAdapter']:
|
|
54
|
+
"""Get a framework adapter by name."""
|
|
55
|
+
# Lazy imports to avoid circular dependency
|
|
56
|
+
from .react import ReactAdapter
|
|
57
|
+
from .angular import AngularAdapter
|
|
58
|
+
from .vue import VueAdapter
|
|
59
|
+
|
|
60
|
+
adapters = {
|
|
61
|
+
'react': ReactAdapter,
|
|
62
|
+
'angular': AngularAdapter,
|
|
63
|
+
'vue': VueAdapter,
|
|
64
|
+
}
|
|
65
|
+
adapter_class = adapters.get(name.lower())
|
|
66
|
+
if adapter_class:
|
|
67
|
+
return adapter_class()
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_available_adapters() -> list:
|
|
72
|
+
"""List available adapter names."""
|
|
73
|
+
return ['react', 'angular', 'vue']
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
React framework adapter for detecting React-specific settling work.
|
|
3
|
+
|
|
4
|
+
Detects React by looking for React DevTools hook or __REACT_DEVTOOLS_GLOBAL_HOOK__.
|
|
5
|
+
Monitors React commits and batch updates to detect when React has finished rendering.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import FrameworkAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReactAdapter(FrameworkAdapter):
|
|
12
|
+
"""
|
|
13
|
+
Adapter for React framework.
|
|
14
|
+
|
|
15
|
+
Hooks into React's commit phase via the DevTools global hook to detect
|
|
16
|
+
when React has finished reconciliation and committed changes to the DOM.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return 'react'
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def detection_script(self) -> str:
|
|
25
|
+
return """
|
|
26
|
+
(function() {
|
|
27
|
+
// Check for React DevTools hook (most reliable)
|
|
28
|
+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) return true;
|
|
29
|
+
|
|
30
|
+
// Check for React fiber root on body
|
|
31
|
+
var root = document.getElementById('root') || document.body;
|
|
32
|
+
for (var key in root) {
|
|
33
|
+
if (key.startsWith('__reactFiber') || key.startsWith('__reactContainer')) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
})();
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def instrumentation_script(self) -> str:
|
|
44
|
+
return """
|
|
45
|
+
(function() {
|
|
46
|
+
if (!window.__waitless__) return false;
|
|
47
|
+
if (window.__waitless__._reactHooked) return true;
|
|
48
|
+
|
|
49
|
+
window.__waitless__.framework = window.__waitless__.framework || {};
|
|
50
|
+
window.__waitless__.framework.react = {
|
|
51
|
+
lastCommitTime: 0,
|
|
52
|
+
pendingUpdates: 0,
|
|
53
|
+
isSettled: true
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
57
|
+
if (hook && hook.onCommitFiberRoot) {
|
|
58
|
+
var originalOnCommit = hook.onCommitFiberRoot;
|
|
59
|
+
hook.onCommitFiberRoot = function(id, root, priority) {
|
|
60
|
+
window.__waitless__.framework.react.lastCommitTime = Date.now();
|
|
61
|
+
window.__waitless__.framework.react.isSettled = false;
|
|
62
|
+
window.__waitless__._log('React commit', { priority: priority });
|
|
63
|
+
|
|
64
|
+
// Mark as settled after a short delay (microtask completion)
|
|
65
|
+
setTimeout(function() {
|
|
66
|
+
window.__waitless__.framework.react.isSettled = true;
|
|
67
|
+
}, 50);
|
|
68
|
+
|
|
69
|
+
return originalOnCommit.apply(this, arguments);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Also try to intercept React's scheduler if available
|
|
74
|
+
if (window.scheduler && window.scheduler.unstable_scheduleCallback) {
|
|
75
|
+
var originalSchedule = window.scheduler.unstable_scheduleCallback;
|
|
76
|
+
window.scheduler.unstable_scheduleCallback = function(priority, callback) {
|
|
77
|
+
window.__waitless__.framework.react.pendingUpdates++;
|
|
78
|
+
var wrappedCallback = function() {
|
|
79
|
+
var result = callback.apply(this, arguments);
|
|
80
|
+
window.__waitless__.framework.react.pendingUpdates--;
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
return originalSchedule.call(this, priority, wrappedCallback);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
window.__waitless__._reactHooked = true;
|
|
88
|
+
window.__waitless__._log('React adapter installed');
|
|
89
|
+
return true;
|
|
90
|
+
})();
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def get_status_script(self) -> str:
|
|
94
|
+
return """
|
|
95
|
+
(function() {
|
|
96
|
+
if (!window.__waitless__ || !window.__waitless__.framework || !window.__waitless__.framework.react) {
|
|
97
|
+
return { stable: true, details: 'React not detected' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
var react = window.__waitless__.framework.react;
|
|
101
|
+
var timeSinceCommit = Date.now() - react.lastCommitTime;
|
|
102
|
+
var isStable = react.isSettled && timeSinceCommit > 100;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
stable: isStable,
|
|
106
|
+
details: isStable
|
|
107
|
+
? 'React idle, last commit ' + timeSinceCommit + 'ms ago'
|
|
108
|
+
: 'React updating, pending=' + react.pendingUpdates
|
|
109
|
+
};
|
|
110
|
+
})();
|
|
111
|
+
"""
|