waitless 0.3.2__tar.gz → 1.0.1__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.2/waitless.egg-info → waitless-1.0.1}/PKG-INFO +56 -10
- {waitless-0.3.2 → waitless-1.0.1}/README.md +52 -6
- {waitless-0.3.2 → waitless-1.0.1}/pyproject.toml +6 -4
- {waitless-0.3.2 → waitless-1.0.1}/waitless/__init__.py +1 -1
- {waitless-0.3.2 → waitless-1.0.1}/waitless/__main__.py +8 -2
- waitless-1.0.1/waitless/adapters/__init__.py +20 -0
- waitless-1.0.1/waitless/adapters/angular.py +137 -0
- waitless-1.0.1/waitless/adapters/base.py +79 -0
- waitless-1.0.1/waitless/adapters/react.py +140 -0
- waitless-1.0.1/waitless/adapters/vue.py +156 -0
- {waitless-0.3.2 → waitless-1.0.1}/waitless/config.py +50 -1
- {waitless-0.3.2 → waitless-1.0.1}/waitless/diagnostics.py +44 -12
- {waitless-0.3.2 → waitless-1.0.1}/waitless/engine.py +141 -14
- {waitless-0.3.2 → waitless-1.0.1}/waitless/instrumentation.py +257 -23
- {waitless-0.3.2 → waitless-1.0.1}/waitless/selenium_integration.py +45 -53
- {waitless-0.3.2 → waitless-1.0.1}/waitless/signals.py +98 -2
- {waitless-0.3.2 → waitless-1.0.1/waitless.egg-info}/PKG-INFO +56 -10
- {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/SOURCES.txt +6 -1
- {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/requires.txt +2 -1
- {waitless-0.3.2 → waitless-1.0.1}/LICENSE +0 -0
- {waitless-0.3.2 → waitless-1.0.1}/setup.cfg +0 -0
- {waitless-0.3.2 → waitless-1.0.1}/waitless/exceptions.py +0 -0
- {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/dependency_links.txt +0 -0
- {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/entry_points.txt +0 -0
- {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
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
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
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
|
|
@@ -11,7 +11,6 @@ Project-URL: Issues, https://github.com/godhiraj-code/waitless/issues
|
|
|
11
11
|
Keywords: selenium,automation,testing,ui-testing,wait,stability,flaky-tests
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -23,18 +22,21 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
23
22
|
Requires-Python: >=3.9
|
|
24
23
|
Description-Content-Type: text/markdown
|
|
25
24
|
License-File: LICENSE
|
|
25
|
+
Requires-Dist: selenium>=4.0
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
31
|
# Waitless
|
|
32
32
|
|
|
33
33
|
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
34
34
|
|
|
35
|
-
**
|
|
35
|
+
**Automatic UI stabilization for Selenium**
|
|
36
|
+
|
|
37
|
+
Reduce explicit waits and sleeps by automatically evaluating multiple UI stability signals.
|
|
38
|
+
|
|
36
39
|
|
|
37
|
-
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
38
40
|
|
|
39
41
|
## Installation
|
|
40
42
|
|
|
@@ -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
|
+
- ✅ Same-origin iframe load readiness (opt-in; not full child-frame signal injection)
|
|
91
96
|
|
|
92
97
|
When you interact, waitless ensures the page is truly ready.
|
|
93
98
|
|
|
@@ -160,7 +165,7 @@ try:
|
|
|
160
165
|
driver.find_element(By.ID, "slow-button").click()
|
|
161
166
|
except StabilizationTimeout as e:
|
|
162
167
|
diagnostics = get_diagnostics(driver)
|
|
163
|
-
print_report(
|
|
168
|
+
print_report(diagnostics) # Print detailed report
|
|
164
169
|
```
|
|
165
170
|
|
|
166
171
|
### CLI Doctor Command
|
|
@@ -214,11 +219,52 @@ 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 | Environment-dependent; run `python -m benchmarks.overhead_test` |
|
|
244
|
+
| Per-poll overhead | Environment-dependent; run the bundled benchmark |
|
|
245
|
+
| Poll interval (default) | 50ms |
|
|
246
|
+
| Typical stabilization | 50-200ms after activity |
|
|
247
|
+
|
|
248
|
+
### Navigation Handling
|
|
249
|
+
|
|
250
|
+
Wrapped `get()`, `refresh()`, `back()`, and `forward()` wait after Selenium's
|
|
251
|
+
synchronous navigation call returns. For SPA route changes initiated by page
|
|
252
|
+
JavaScript, Waitless validates/re-injects instrumentation on the next wait:
|
|
253
|
+
|
|
254
|
+
1. Checks `__waitless__.isAlive()` before each wait
|
|
255
|
+
2. Detects URL changes via `driver.current_url`
|
|
256
|
+
3. Re-injects if instrumentation is missing
|
|
257
|
+
|
|
258
|
+
This does not observe routes continuously, and cross-origin iframe internals remain
|
|
259
|
+
outside the browser same-origin boundary.
|
|
260
|
+
|
|
261
|
+
`find_elements()` keeps Selenium's immediate-empty lookup semantics: after the
|
|
262
|
+
page-stability wait, it performs one lookup and returns `[]` when there are no matches.
|
|
263
|
+
|
|
264
|
+
## Current Limitations
|
|
218
265
|
|
|
219
|
-
- **Selenium only** - Playwright support planned
|
|
266
|
+
- **Selenium only** - Playwright support planned
|
|
220
267
|
- **Sync only** - No async/await support yet
|
|
221
|
-
- **Main frame only** - iframes not monitored
|
|
222
268
|
- **No Service Workers** - SW network requests not intercepted
|
|
223
269
|
|
|
224
270
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Automatic UI stabilization for Selenium**
|
|
6
|
+
|
|
7
|
+
Reduce explicit waits and sleeps by automatically evaluating multiple UI stability signals.
|
|
8
|
+
|
|
6
9
|
|
|
7
|
-
Eliminate explicit waits and sleeps by automatically detecting true UI stability.
|
|
8
10
|
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
@@ -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
|
+
- ✅ Same-origin iframe load readiness (opt-in; not full child-frame signal injection)
|
|
61
66
|
|
|
62
67
|
When you interact, waitless ensures the page is truly ready.
|
|
63
68
|
|
|
@@ -130,7 +135,7 @@ try:
|
|
|
130
135
|
driver.find_element(By.ID, "slow-button").click()
|
|
131
136
|
except StabilizationTimeout as e:
|
|
132
137
|
diagnostics = get_diagnostics(driver)
|
|
133
|
-
print_report(
|
|
138
|
+
print_report(diagnostics) # Print detailed report
|
|
134
139
|
```
|
|
135
140
|
|
|
136
141
|
### CLI Doctor Command
|
|
@@ -184,11 +189,52 @@ 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 | Environment-dependent; run `python -m benchmarks.overhead_test` |
|
|
214
|
+
| Per-poll overhead | Environment-dependent; run the bundled benchmark |
|
|
215
|
+
| Poll interval (default) | 50ms |
|
|
216
|
+
| Typical stabilization | 50-200ms after activity |
|
|
217
|
+
|
|
218
|
+
### Navigation Handling
|
|
219
|
+
|
|
220
|
+
Wrapped `get()`, `refresh()`, `back()`, and `forward()` wait after Selenium's
|
|
221
|
+
synchronous navigation call returns. For SPA route changes initiated by page
|
|
222
|
+
JavaScript, Waitless validates/re-injects instrumentation on the next wait:
|
|
223
|
+
|
|
224
|
+
1. Checks `__waitless__.isAlive()` before each wait
|
|
225
|
+
2. Detects URL changes via `driver.current_url`
|
|
226
|
+
3. Re-injects if instrumentation is missing
|
|
227
|
+
|
|
228
|
+
This does not observe routes continuously, and cross-origin iframe internals remain
|
|
229
|
+
outside the browser same-origin boundary.
|
|
230
|
+
|
|
231
|
+
`find_elements()` keeps Selenium's immediate-empty lookup semantics: after the
|
|
232
|
+
page-stability wait, it performs one lookup and returns `[]` when there are no matches.
|
|
233
|
+
|
|
234
|
+
## Current Limitations
|
|
188
235
|
|
|
189
|
-
- **Selenium only** - Playwright support planned
|
|
236
|
+
- **Selenium only** - Playwright support planned
|
|
190
237
|
- **Sync only** - No async/await support yet
|
|
191
|
-
- **Main frame only** - iframes not monitored
|
|
192
238
|
- **No Service Workers** - SW network requests not intercepted
|
|
193
239
|
|
|
194
240
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "waitless"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Eliminate explicit waits in UI automation by detecting true UI stability"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Dhiraj Das", email = "dhirajdas.666@gmail.com"}
|
|
@@ -24,7 +24,6 @@ keywords = [
|
|
|
24
24
|
classifiers = [
|
|
25
25
|
"Development Status :: 4 - Beta",
|
|
26
26
|
"Intended Audience :: Developers",
|
|
27
|
-
"License :: OSI Approved :: MIT License",
|
|
28
27
|
"Operating System :: OS Independent",
|
|
29
28
|
"Programming Language :: Python :: 3",
|
|
30
29
|
"Programming Language :: Python :: 3.9",
|
|
@@ -34,6 +33,9 @@ classifiers = [
|
|
|
34
33
|
"Topic :: Software Development :: Testing",
|
|
35
34
|
"Topic :: Software Development :: Quality Assurance",
|
|
36
35
|
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"selenium>=4.0",
|
|
38
|
+
]
|
|
37
39
|
|
|
38
40
|
[project.urls]
|
|
39
41
|
Homepage = "https://www.dhirajdas.dev"
|
|
@@ -47,7 +49,7 @@ waitless = "waitless.__main__:main"
|
|
|
47
49
|
[project.optional-dependencies]
|
|
48
50
|
dev = [
|
|
49
51
|
"pytest>=7.0",
|
|
50
|
-
"
|
|
52
|
+
"build>=1.0",
|
|
51
53
|
]
|
|
52
54
|
|
|
53
55
|
[tool.setuptools.packages.find]
|
|
@@ -72,11 +72,16 @@ def run_doctor(args):
|
|
|
72
72
|
else:
|
|
73
73
|
# Show usage instructions
|
|
74
74
|
print("+-" + "-" * 66 + "-+")
|
|
75
|
-
print("|" + "WAITLESS DOCTOR".center(66) + "|")
|
|
75
|
+
print("|" + "WAITLESS DOCTOR v1.0".center(66) + "|")
|
|
76
76
|
print("+-" + "-" * 66 + "-+")
|
|
77
77
|
print("|".ljust(67) + "|")
|
|
78
78
|
print("| The doctor command analyzes stability diagnostics.".ljust(67) + "|")
|
|
79
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) + "|")
|
|
80
85
|
print("| USAGE OPTIONS:".ljust(67) + "|")
|
|
81
86
|
print("|".ljust(67) + "|")
|
|
82
87
|
print("| 1. From a diagnostic file:".ljust(67) + "|")
|
|
@@ -90,7 +95,8 @@ def run_doctor(args):
|
|
|
90
95
|
print("| try:".ljust(67) + "|")
|
|
91
96
|
print("| driver.find_element(...).click()".ljust(67) + "|")
|
|
92
97
|
print("| except StabilizationTimeout as e:".ljust(67) + "|")
|
|
93
|
-
print("|
|
|
98
|
+
print("| diagnostics = get_diagnostics(driver)".ljust(67) + "|")
|
|
99
|
+
print("| print_report(diagnostics)".ljust(67) + "|")
|
|
94
100
|
print("|".ljust(67) + "|")
|
|
95
101
|
print("| 3. Export diagnostics for CI:".ljust(67) + "|")
|
|
96
102
|
print("|".ljust(67) + "|")
|
|
@@ -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,137 @@
|
|
|
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
|
+
var angular = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.angular;
|
|
60
|
+
if (angular) {
|
|
61
|
+
angular.isStable = true;
|
|
62
|
+
angular.lastStableTime = Date.now();
|
|
63
|
+
window.__waitless__._log('Angular became stable');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Also try to hook into Zone.js if available
|
|
70
|
+
if (window.Zone && window.Zone.current) {
|
|
71
|
+
var originalRun = Zone.prototype.run;
|
|
72
|
+
window.__waitless__._angularOriginals = {
|
|
73
|
+
zonePrototype: Zone.prototype,
|
|
74
|
+
run: originalRun
|
|
75
|
+
};
|
|
76
|
+
Zone.prototype.run = function(callback, applyThis, applyArgs) {
|
|
77
|
+
var angular = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.angular;
|
|
78
|
+
var tracked = this.name === 'angular' && angular;
|
|
79
|
+
if (tracked) {
|
|
80
|
+
angular.isStable = false;
|
|
81
|
+
angular.pendingTasks++;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return originalRun.apply(this, arguments);
|
|
85
|
+
} finally {
|
|
86
|
+
var current = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.angular;
|
|
87
|
+
if (tracked && current) {
|
|
88
|
+
current.pendingTasks--;
|
|
89
|
+
if (current.pendingTasks === 0) {
|
|
90
|
+
current.isStable = true;
|
|
91
|
+
current.lastStableTime = Date.now();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
window.__waitless__._angularHooked = true;
|
|
99
|
+
window.__waitless__._log('Angular adapter installed');
|
|
100
|
+
return true;
|
|
101
|
+
})();
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def uninstall_script(self) -> str:
|
|
106
|
+
return """
|
|
107
|
+
(function() {
|
|
108
|
+
if (!window.__waitless__) return true;
|
|
109
|
+
var originals = window.__waitless__._angularOriginals || {};
|
|
110
|
+
if (originals.zonePrototype && originals.run) {
|
|
111
|
+
originals.zonePrototype.run = originals.run;
|
|
112
|
+
}
|
|
113
|
+
if (window.__waitless__.framework) delete window.__waitless__.framework.angular;
|
|
114
|
+
delete window.__waitless__._angularOriginals;
|
|
115
|
+
delete window.__waitless__._angularHooked;
|
|
116
|
+
return true;
|
|
117
|
+
})();
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def get_status_script(self) -> str:
|
|
121
|
+
return """
|
|
122
|
+
(function() {
|
|
123
|
+
if (!window.__waitless__ || !window.__waitless__.framework || !window.__waitless__.framework.angular) {
|
|
124
|
+
return { stable: true, details: 'Angular not detected' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var angular = window.__waitless__.framework.angular;
|
|
128
|
+
var timeSinceStable = Date.now() - angular.lastStableTime;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
stable: angular.isStable,
|
|
132
|
+
details: angular.isStable
|
|
133
|
+
? 'NgZone stable, last activity ' + timeSinceStable + 'ms ago'
|
|
134
|
+
: 'NgZone unstable, pending=' + angular.pendingTasks
|
|
135
|
+
};
|
|
136
|
+
})();
|
|
137
|
+
"""
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
@property
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def uninstall_script(self) -> str:
|
|
47
|
+
"""JavaScript that restores every function patched by this adapter."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_status_script(self) -> str:
|
|
52
|
+
"""
|
|
53
|
+
JavaScript that returns the current framework stability status.
|
|
54
|
+
Should return { stable: bool, details: string }.
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_adapter(name: str) -> Optional['FrameworkAdapter']:
|
|
60
|
+
"""Get a framework adapter by name."""
|
|
61
|
+
# Lazy imports to avoid circular dependency
|
|
62
|
+
from .react import ReactAdapter
|
|
63
|
+
from .angular import AngularAdapter
|
|
64
|
+
from .vue import VueAdapter
|
|
65
|
+
|
|
66
|
+
adapters = {
|
|
67
|
+
'react': ReactAdapter,
|
|
68
|
+
'angular': AngularAdapter,
|
|
69
|
+
'vue': VueAdapter,
|
|
70
|
+
}
|
|
71
|
+
adapter_class = adapters.get(name.lower())
|
|
72
|
+
if adapter_class:
|
|
73
|
+
return adapter_class()
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_available_adapters() -> list:
|
|
78
|
+
"""List available adapter names."""
|
|
79
|
+
return ['react', 'angular', 'vue']
|
|
@@ -0,0 +1,140 @@
|
|
|
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 originals = window.__waitless__._reactOriginals = {};
|
|
57
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
58
|
+
if (hook && hook.onCommitFiberRoot) {
|
|
59
|
+
var originalOnCommit = hook.onCommitFiberRoot;
|
|
60
|
+
originals.hook = hook;
|
|
61
|
+
originals.onCommitFiberRoot = originalOnCommit;
|
|
62
|
+
hook.onCommitFiberRoot = function(id, root, priority) {
|
|
63
|
+
var react = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.react;
|
|
64
|
+
if (react) {
|
|
65
|
+
react.lastCommitTime = Date.now();
|
|
66
|
+
react.isSettled = false;
|
|
67
|
+
window.__waitless__._log('React commit', { priority: priority });
|
|
68
|
+
|
|
69
|
+
setTimeout(function() {
|
|
70
|
+
var current = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.react;
|
|
71
|
+
if (current) current.isSettled = true;
|
|
72
|
+
}, 50);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return originalOnCommit.apply(this, arguments);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Also try to intercept React's scheduler if available
|
|
80
|
+
if (window.scheduler && window.scheduler.unstable_scheduleCallback) {
|
|
81
|
+
var originalSchedule = window.scheduler.unstable_scheduleCallback;
|
|
82
|
+
originals.scheduler = window.scheduler;
|
|
83
|
+
originals.scheduleCallback = originalSchedule;
|
|
84
|
+
window.scheduler.unstable_scheduleCallback = function(priority, callback) {
|
|
85
|
+
var react = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.react;
|
|
86
|
+
if (react) react.pendingUpdates++;
|
|
87
|
+
var wrappedCallback = function() {
|
|
88
|
+
var result = callback.apply(this, arguments);
|
|
89
|
+
var current = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.react;
|
|
90
|
+
if (current) current.pendingUpdates--;
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
93
|
+
return originalSchedule.call(this, priority, wrappedCallback);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
window.__waitless__._reactHooked = true;
|
|
98
|
+
window.__waitless__._log('React adapter installed');
|
|
99
|
+
return true;
|
|
100
|
+
})();
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def uninstall_script(self) -> str:
|
|
105
|
+
return """
|
|
106
|
+
(function() {
|
|
107
|
+
if (!window.__waitless__) return true;
|
|
108
|
+
var originals = window.__waitless__._reactOriginals || {};
|
|
109
|
+
if (originals.hook && originals.onCommitFiberRoot) {
|
|
110
|
+
originals.hook.onCommitFiberRoot = originals.onCommitFiberRoot;
|
|
111
|
+
}
|
|
112
|
+
if (originals.scheduler && originals.scheduleCallback) {
|
|
113
|
+
originals.scheduler.unstable_scheduleCallback = originals.scheduleCallback;
|
|
114
|
+
}
|
|
115
|
+
if (window.__waitless__.framework) delete window.__waitless__.framework.react;
|
|
116
|
+
delete window.__waitless__._reactOriginals;
|
|
117
|
+
delete window.__waitless__._reactHooked;
|
|
118
|
+
return true;
|
|
119
|
+
})();
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def get_status_script(self) -> str:
|
|
123
|
+
return """
|
|
124
|
+
(function() {
|
|
125
|
+
if (!window.__waitless__ || !window.__waitless__.framework || !window.__waitless__.framework.react) {
|
|
126
|
+
return { stable: true, details: 'React not detected' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
var react = window.__waitless__.framework.react;
|
|
130
|
+
var timeSinceCommit = Date.now() - react.lastCommitTime;
|
|
131
|
+
var isStable = react.isSettled && timeSinceCommit > 100;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
stable: isStable,
|
|
135
|
+
details: isStable
|
|
136
|
+
? 'React idle, last commit ' + timeSinceCommit + 'ms ago'
|
|
137
|
+
: 'React updating, pending=' + react.pendingUpdates
|
|
138
|
+
};
|
|
139
|
+
})();
|
|
140
|
+
"""
|