waitless 1.0.0__tar.gz → 1.0.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-1.0.0/waitless.egg-info → waitless-1.0.2}/PKG-INFO +28 -16
- {waitless-1.0.0 → waitless-1.0.2}/README.md +21 -9
- {waitless-1.0.0 → waitless-1.0.2}/pyproject.toml +9 -7
- {waitless-1.0.0 → waitless-1.0.2}/waitless/__init__.py +1 -1
- {waitless-1.0.0 → waitless-1.0.2}/waitless/__main__.py +2 -1
- {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/angular.py +41 -13
- {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/base.py +6 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/react.py +40 -11
- {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/vue.py +59 -20
- {waitless-1.0.0 → waitless-1.0.2}/waitless/config.py +23 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless/diagnostics.py +18 -12
- {waitless-1.0.0 → waitless-1.0.2}/waitless/engine.py +141 -14
- {waitless-1.0.0 → waitless-1.0.2}/waitless/instrumentation.py +35 -23
- {waitless-1.0.0 → waitless-1.0.2}/waitless/selenium_integration.py +45 -53
- {waitless-1.0.0 → waitless-1.0.2}/waitless/signals.py +37 -2
- {waitless-1.0.0 → waitless-1.0.2/waitless.egg-info}/PKG-INFO +28 -16
- {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/requires.txt +2 -1
- {waitless-1.0.0 → waitless-1.0.2}/LICENSE +0 -0
- {waitless-1.0.0 → waitless-1.0.2}/setup.cfg +0 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/__init__.py +0 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless/exceptions.py +0 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/SOURCES.txt +0 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/dependency_links.txt +0 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/entry_points.txt +0 -0
- {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/top_level.txt +0 -0
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.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
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://www.dhirajdas.dev
|
|
8
|
-
Project-URL: Documentation, https://
|
|
9
|
-
Project-URL: Repository, https://github.com/godhiraj-code/waitless
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.dhirajdas.dev/project/waitless
|
|
8
|
+
Project-URL: Documentation, https://www.dhirajdas.dev/project/waitless
|
|
9
|
+
Project-URL: Repository, https://github.com/godhiraj-code/waitless
|
|
10
10
|
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,20 +22,23 @@ 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
36
|
|
|
37
|
-
|
|
37
|
+
Reduce explicit waits and sleeps by automatically evaluating multiple UI stability signals.
|
|
38
38
|
|
|
39
|
+
[Watch the demo and read the architecture case study](https://www.dhirajdas.dev/project/waitless) · [Install from PyPI](https://pypi.org/project/waitless/)
|
|
39
40
|
|
|
41
|
+

|
|
40
42
|
|
|
41
43
|
## Installation
|
|
42
44
|
|
|
@@ -92,7 +94,7 @@ Waitless monitors the **entire page** for stability signals:
|
|
|
92
94
|
- ✅ Layout stability (element movement)
|
|
93
95
|
- ✅ WebSocket/SSE activity (opt-in)
|
|
94
96
|
- ✅ Framework hooks (React/Angular/Vue, opt-in)
|
|
95
|
-
- ✅ iframe
|
|
97
|
+
- ✅ Same-origin iframe load readiness (opt-in; not full child-frame signal injection)
|
|
96
98
|
|
|
97
99
|
When you interact, waitless ensures the page is truly ready.
|
|
98
100
|
|
|
@@ -165,7 +167,7 @@ try:
|
|
|
165
167
|
driver.find_element(By.ID, "slow-button").click()
|
|
166
168
|
except StabilizationTimeout as e:
|
|
167
169
|
diagnostics = get_diagnostics(driver)
|
|
168
|
-
print_report(
|
|
170
|
+
print_report(diagnostics) # Print detailed report
|
|
169
171
|
```
|
|
170
172
|
|
|
171
173
|
### CLI Doctor Command
|
|
@@ -240,20 +242,26 @@ config = StabilizationConfig(
|
|
|
240
242
|
|
|
241
243
|
| Metric | Typical Value |
|
|
242
244
|
|--------|---------------|
|
|
243
|
-
| Instrumentation injection |
|
|
244
|
-
| Per-poll overhead |
|
|
245
|
+
| Instrumentation injection | Environment-dependent; run `python -m benchmarks.overhead_test` |
|
|
246
|
+
| Per-poll overhead | Environment-dependent; run the bundled benchmark |
|
|
245
247
|
| Poll interval (default) | 50ms |
|
|
246
248
|
| Typical stabilization | 50-200ms after activity |
|
|
247
249
|
|
|
248
|
-
###
|
|
250
|
+
### Navigation Handling
|
|
249
251
|
|
|
250
|
-
|
|
252
|
+
Wrapped `get()`, `refresh()`, `back()`, and `forward()` wait after Selenium's
|
|
253
|
+
synchronous navigation call returns. For SPA route changes initiated by page
|
|
254
|
+
JavaScript, Waitless validates/re-injects instrumentation on the next wait:
|
|
251
255
|
|
|
252
256
|
1. Checks `__waitless__.isAlive()` before each wait
|
|
253
257
|
2. Detects URL changes via `driver.current_url`
|
|
254
258
|
3. Re-injects if instrumentation is missing
|
|
255
259
|
|
|
256
|
-
This
|
|
260
|
+
This does not observe routes continuously, and cross-origin iframe internals remain
|
|
261
|
+
outside the browser same-origin boundary.
|
|
262
|
+
|
|
263
|
+
`find_elements()` keeps Selenium's immediate-empty lookup semantics: after the
|
|
264
|
+
page-stability wait, it performs one lookup and returns `[]` when there are no matches.
|
|
257
265
|
|
|
258
266
|
## Current Limitations
|
|
259
267
|
|
|
@@ -286,3 +294,7 @@ See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
|
286
294
|
## License
|
|
287
295
|
|
|
288
296
|
MIT
|
|
297
|
+
|
|
298
|
+
## Try It on a Real Flaky Flow
|
|
299
|
+
|
|
300
|
+
Run Waitless against one Selenium flow that currently uses sleeps or retries, then compare failures and elapsed time before and after. Share a sanitized result in an issue. If it removes a sleep or retry, [star the repository](https://github.com/godhiraj-code/waitless). The [full case study](https://www.dhirajdas.dev/project/waitless) explains the architecture and trade-offs.
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Automatic UI stabilization for Selenium**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Reduce explicit waits and sleeps by automatically evaluating multiple UI stability signals.
|
|
8
8
|
|
|
9
|
+
[Watch the demo and read the architecture case study](https://www.dhirajdas.dev/project/waitless) · [Install from PyPI](https://pypi.org/project/waitless/)
|
|
9
10
|
|
|
11
|
+

|
|
10
12
|
|
|
11
13
|
## Installation
|
|
12
14
|
|
|
@@ -62,7 +64,7 @@ Waitless monitors the **entire page** for stability signals:
|
|
|
62
64
|
- ✅ Layout stability (element movement)
|
|
63
65
|
- ✅ WebSocket/SSE activity (opt-in)
|
|
64
66
|
- ✅ Framework hooks (React/Angular/Vue, opt-in)
|
|
65
|
-
- ✅ iframe
|
|
67
|
+
- ✅ Same-origin iframe load readiness (opt-in; not full child-frame signal injection)
|
|
66
68
|
|
|
67
69
|
When you interact, waitless ensures the page is truly ready.
|
|
68
70
|
|
|
@@ -135,7 +137,7 @@ try:
|
|
|
135
137
|
driver.find_element(By.ID, "slow-button").click()
|
|
136
138
|
except StabilizationTimeout as e:
|
|
137
139
|
diagnostics = get_diagnostics(driver)
|
|
138
|
-
print_report(
|
|
140
|
+
print_report(diagnostics) # Print detailed report
|
|
139
141
|
```
|
|
140
142
|
|
|
141
143
|
### CLI Doctor Command
|
|
@@ -210,20 +212,26 @@ config = StabilizationConfig(
|
|
|
210
212
|
|
|
211
213
|
| Metric | Typical Value |
|
|
212
214
|
|--------|---------------|
|
|
213
|
-
| Instrumentation injection |
|
|
214
|
-
| Per-poll overhead |
|
|
215
|
+
| Instrumentation injection | Environment-dependent; run `python -m benchmarks.overhead_test` |
|
|
216
|
+
| Per-poll overhead | Environment-dependent; run the bundled benchmark |
|
|
215
217
|
| Poll interval (default) | 50ms |
|
|
216
218
|
| Typical stabilization | 50-200ms after activity |
|
|
217
219
|
|
|
218
|
-
###
|
|
220
|
+
### Navigation Handling
|
|
219
221
|
|
|
220
|
-
|
|
222
|
+
Wrapped `get()`, `refresh()`, `back()`, and `forward()` wait after Selenium's
|
|
223
|
+
synchronous navigation call returns. For SPA route changes initiated by page
|
|
224
|
+
JavaScript, Waitless validates/re-injects instrumentation on the next wait:
|
|
221
225
|
|
|
222
226
|
1. Checks `__waitless__.isAlive()` before each wait
|
|
223
227
|
2. Detects URL changes via `driver.current_url`
|
|
224
228
|
3. Re-injects if instrumentation is missing
|
|
225
229
|
|
|
226
|
-
This
|
|
230
|
+
This does not observe routes continuously, and cross-origin iframe internals remain
|
|
231
|
+
outside the browser same-origin boundary.
|
|
232
|
+
|
|
233
|
+
`find_elements()` keeps Selenium's immediate-empty lookup semantics: after the
|
|
234
|
+
page-stability wait, it performs one lookup and returns `[]` when there are no matches.
|
|
227
235
|
|
|
228
236
|
## Current Limitations
|
|
229
237
|
|
|
@@ -256,3 +264,7 @@ See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
|
256
264
|
## License
|
|
257
265
|
|
|
258
266
|
MIT
|
|
267
|
+
|
|
268
|
+
## Try It on a Real Flaky Flow
|
|
269
|
+
|
|
270
|
+
Run Waitless against one Selenium flow that currently uses sleeps or retries, then compare failures and elapsed time before and after. Share a sanitized result in an issue. If it removes a sleep or retry, [star the repository](https://github.com/godhiraj-code/waitless). The [full case study](https://www.dhirajdas.dev/project/waitless) explains the architecture and trade-offs.
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "waitless"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
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,11 +33,14 @@ 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
|
-
Homepage = "https://www.dhirajdas.dev"
|
|
40
|
-
Documentation = "https://
|
|
41
|
-
Repository = "https://github.com/godhiraj-code/waitless
|
|
41
|
+
Homepage = "https://www.dhirajdas.dev/project/waitless"
|
|
42
|
+
Documentation = "https://www.dhirajdas.dev/project/waitless"
|
|
43
|
+
Repository = "https://github.com/godhiraj-code/waitless"
|
|
42
44
|
Issues = "https://github.com/godhiraj-code/waitless/issues"
|
|
43
45
|
|
|
44
46
|
[project.scripts]
|
|
@@ -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]
|
|
@@ -95,7 +95,8 @@ def run_doctor(args):
|
|
|
95
95
|
print("| try:".ljust(67) + "|")
|
|
96
96
|
print("| driver.find_element(...).click()".ljust(67) + "|")
|
|
97
97
|
print("| except StabilizationTimeout as e:".ljust(67) + "|")
|
|
98
|
-
print("|
|
|
98
|
+
print("| diagnostics = get_diagnostics(driver)".ljust(67) + "|")
|
|
99
|
+
print("| print_report(diagnostics)".ljust(67) + "|")
|
|
99
100
|
print("|".ljust(67) + "|")
|
|
100
101
|
print("| 3. Export diagnostics for CI:".ljust(67) + "|")
|
|
101
102
|
print("|".ljust(67) + "|")
|
|
@@ -56,9 +56,12 @@ class AngularAdapter(FrameworkAdapter):
|
|
|
56
56
|
if (testability && testability.length > 0) {
|
|
57
57
|
testability.forEach(function(t) {
|
|
58
58
|
t.whenStable(function() {
|
|
59
|
-
window.__waitless__.framework.angular
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
65
|
});
|
|
63
66
|
});
|
|
64
67
|
}
|
|
@@ -66,20 +69,29 @@ class AngularAdapter(FrameworkAdapter):
|
|
|
66
69
|
// Also try to hook into Zone.js if available
|
|
67
70
|
if (window.Zone && window.Zone.current) {
|
|
68
71
|
var originalRun = Zone.prototype.run;
|
|
72
|
+
window.__waitless__._angularOriginals = {
|
|
73
|
+
zonePrototype: Zone.prototype,
|
|
74
|
+
run: originalRun
|
|
75
|
+
};
|
|
69
76
|
Zone.prototype.run = function(callback, applyThis, applyArgs) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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++;
|
|
73
82
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
|
-
return result;
|
|
83
95
|
};
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -89,6 +101,22 @@ class AngularAdapter(FrameworkAdapter):
|
|
|
89
101
|
})();
|
|
90
102
|
"""
|
|
91
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
|
+
|
|
92
120
|
def get_status_script(self) -> str:
|
|
93
121
|
return """
|
|
94
122
|
(function() {
|
|
@@ -41,6 +41,12 @@ class FrameworkAdapter(ABC):
|
|
|
41
41
|
"""
|
|
42
42
|
pass
|
|
43
43
|
|
|
44
|
+
@property
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def uninstall_script(self) -> str:
|
|
47
|
+
"""JavaScript that restores every function patched by this adapter."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
44
50
|
@abstractmethod
|
|
45
51
|
def get_status_script(self) -> str:
|
|
46
52
|
"""
|
|
@@ -53,19 +53,25 @@ class ReactAdapter(FrameworkAdapter):
|
|
|
53
53
|
isSettled: true
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
var originals = window.__waitless__._reactOriginals = {};
|
|
56
57
|
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
57
58
|
if (hook && hook.onCommitFiberRoot) {
|
|
58
59
|
var originalOnCommit = hook.onCommitFiberRoot;
|
|
60
|
+
originals.hook = hook;
|
|
61
|
+
originals.onCommitFiberRoot = originalOnCommit;
|
|
59
62
|
hook.onCommitFiberRoot = function(id, root, priority) {
|
|
60
|
-
window.__waitless__.framework
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
|
|
69
75
|
return originalOnCommit.apply(this, arguments);
|
|
70
76
|
};
|
|
71
77
|
}
|
|
@@ -73,11 +79,15 @@ class ReactAdapter(FrameworkAdapter):
|
|
|
73
79
|
// Also try to intercept React's scheduler if available
|
|
74
80
|
if (window.scheduler && window.scheduler.unstable_scheduleCallback) {
|
|
75
81
|
var originalSchedule = window.scheduler.unstable_scheduleCallback;
|
|
82
|
+
originals.scheduler = window.scheduler;
|
|
83
|
+
originals.scheduleCallback = originalSchedule;
|
|
76
84
|
window.scheduler.unstable_scheduleCallback = function(priority, callback) {
|
|
77
|
-
window.__waitless__.framework.react
|
|
85
|
+
var react = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.react;
|
|
86
|
+
if (react) react.pendingUpdates++;
|
|
78
87
|
var wrappedCallback = function() {
|
|
79
88
|
var result = callback.apply(this, arguments);
|
|
80
|
-
window.__waitless__.framework.react
|
|
89
|
+
var current = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.react;
|
|
90
|
+
if (current) current.pendingUpdates--;
|
|
81
91
|
return result;
|
|
82
92
|
};
|
|
83
93
|
return originalSchedule.call(this, priority, wrappedCallback);
|
|
@@ -90,6 +100,25 @@ class ReactAdapter(FrameworkAdapter):
|
|
|
90
100
|
})();
|
|
91
101
|
"""
|
|
92
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
|
+
|
|
93
122
|
def get_status_script(self) -> str:
|
|
94
123
|
return """
|
|
95
124
|
(function() {
|
|
@@ -57,34 +57,53 @@ class VueAdapter(FrameworkAdapter):
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
var hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
60
|
+
var originals = window.__waitless__._vueOriginals = {};
|
|
60
61
|
if (hook) {
|
|
61
|
-
// Vue 3 DevTools hook
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
// Vue 3 DevTools hook. Reuse a stable handler if the hook has no off().
|
|
63
|
+
if (hook.on) {
|
|
64
|
+
var updateHandler = hook.__waitlessVueUpdateHandler;
|
|
65
|
+
if (!updateHandler) {
|
|
66
|
+
updateHandler = function() {
|
|
67
|
+
var vue = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.vue;
|
|
68
|
+
if (!vue) return;
|
|
69
|
+
vue.lastUpdateTime = Date.now();
|
|
70
|
+
vue.isSettled = false;
|
|
71
|
+
window.__waitless__._log('Vue component updated');
|
|
72
|
+
|
|
73
|
+
setTimeout(function() {
|
|
74
|
+
var current = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.vue;
|
|
75
|
+
if (current) current.isSettled = true;
|
|
76
|
+
}, 50);
|
|
77
|
+
};
|
|
78
|
+
hook.__waitlessVueUpdateHandler = updateHandler;
|
|
79
|
+
hook.on('component:updated', updateHandler);
|
|
80
|
+
}
|
|
81
|
+
originals.hook = hook;
|
|
82
|
+
originals.updateHandler = updateHandler;
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
// Vue 2 compatibility
|
|
73
86
|
if (hook.Vue && hook.Vue.nextTick) {
|
|
74
87
|
var originalNextTick = hook.Vue.nextTick;
|
|
88
|
+
originals.vue = hook.Vue;
|
|
89
|
+
originals.nextTick = originalNextTick;
|
|
75
90
|
hook.Vue.nextTick = function(callback, context) {
|
|
76
|
-
window.__waitless__.framework.vue
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
var vue = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.vue;
|
|
92
|
+
if (vue) {
|
|
93
|
+
vue.pendingTicks++;
|
|
94
|
+
vue.isSettled = false;
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
var wrappedCallback = function() {
|
|
80
|
-
window.__waitless__.framework.vue
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
var current = window.__waitless__ && window.__waitless__.framework && window.__waitless__.framework.vue;
|
|
99
|
+
if (current) {
|
|
100
|
+
current.pendingTicks--;
|
|
101
|
+
current.lastUpdateTime = Date.now();
|
|
102
|
+
if (current.pendingTicks === 0) current.isSettled = true;
|
|
84
103
|
}
|
|
85
|
-
if (callback) callback.apply(this, arguments);
|
|
104
|
+
if (callback) return callback.apply(this, arguments);
|
|
86
105
|
};
|
|
87
|
-
|
|
106
|
+
|
|
88
107
|
return originalNextTick.call(this, wrappedCallback, context);
|
|
89
108
|
};
|
|
90
109
|
}
|
|
@@ -96,6 +115,26 @@ class VueAdapter(FrameworkAdapter):
|
|
|
96
115
|
})();
|
|
97
116
|
"""
|
|
98
117
|
|
|
118
|
+
@property
|
|
119
|
+
def uninstall_script(self) -> str:
|
|
120
|
+
return """
|
|
121
|
+
(function() {
|
|
122
|
+
if (!window.__waitless__) return true;
|
|
123
|
+
var originals = window.__waitless__._vueOriginals || {};
|
|
124
|
+
if (originals.vue && originals.nextTick) {
|
|
125
|
+
originals.vue.nextTick = originals.nextTick;
|
|
126
|
+
}
|
|
127
|
+
if (originals.hook && originals.updateHandler && originals.hook.off) {
|
|
128
|
+
originals.hook.off('component:updated', originals.updateHandler);
|
|
129
|
+
delete originals.hook.__waitlessVueUpdateHandler;
|
|
130
|
+
}
|
|
131
|
+
if (window.__waitless__.framework) delete window.__waitless__.framework.vue;
|
|
132
|
+
delete window.__waitless__._vueOriginals;
|
|
133
|
+
delete window.__waitless__._vueHooked;
|
|
134
|
+
return true;
|
|
135
|
+
})();
|
|
136
|
+
"""
|
|
137
|
+
|
|
99
138
|
def get_status_script(self) -> str:
|
|
100
139
|
return """
|
|
101
140
|
(function() {
|
|
@@ -107,6 +107,12 @@ class StabilizationConfig:
|
|
|
107
107
|
raise ConfigurationError(
|
|
108
108
|
f"dom_settle_time must be non-negative, got {self.dom_settle_time}"
|
|
109
109
|
)
|
|
110
|
+
|
|
111
|
+
if self.mutation_rate_threshold < 0:
|
|
112
|
+
raise ConfigurationError(
|
|
113
|
+
"mutation_rate_threshold must be non-negative, "
|
|
114
|
+
f"got {self.mutation_rate_threshold}"
|
|
115
|
+
)
|
|
110
116
|
|
|
111
117
|
if self.network_idle_threshold < 0:
|
|
112
118
|
raise ConfigurationError(
|
|
@@ -122,11 +128,28 @@ class StabilizationConfig:
|
|
|
122
128
|
raise ConfigurationError(
|
|
123
129
|
f"poll_interval ({self.poll_interval}) cannot exceed timeout ({self.timeout})"
|
|
124
130
|
)
|
|
131
|
+
|
|
132
|
+
if self.websocket_quiet_time < 0:
|
|
133
|
+
raise ConfigurationError(
|
|
134
|
+
"websocket_quiet_time must be non-negative, "
|
|
135
|
+
f"got {self.websocket_quiet_time}"
|
|
136
|
+
)
|
|
125
137
|
|
|
126
138
|
if self.strictness not in ('strict', 'normal', 'relaxed'):
|
|
127
139
|
raise ConfigurationError(
|
|
128
140
|
f"strictness must be 'strict', 'normal', or 'relaxed', got '{self.strictness}'"
|
|
129
141
|
)
|
|
142
|
+
|
|
143
|
+
supported_hooks = {'react', 'angular', 'vue'}
|
|
144
|
+
normalized_hooks = []
|
|
145
|
+
for hook in self.framework_hooks:
|
|
146
|
+
if not isinstance(hook, str) or hook.lower() not in supported_hooks:
|
|
147
|
+
raise ConfigurationError(
|
|
148
|
+
f"unsupported framework hook {hook!r}; expected one of "
|
|
149
|
+
f"{sorted(supported_hooks)}"
|
|
150
|
+
)
|
|
151
|
+
normalized_hooks.append(hook.lower())
|
|
152
|
+
self.framework_hooks = list(dict.fromkeys(normalized_hooks))
|
|
130
153
|
|
|
131
154
|
def with_overrides(self, **kwargs) -> 'StabilizationConfig':
|
|
132
155
|
"""
|
|
@@ -166,14 +166,14 @@ class DiagnosticReport:
|
|
|
166
166
|
)
|
|
167
167
|
|
|
168
168
|
if suggestions:
|
|
169
|
-
lines.append("
|
|
170
|
-
lines.append("
|
|
171
|
-
lines.append("
|
|
169
|
+
lines.append("+-" + "-" * 66 + "-+")
|
|
170
|
+
lines.append("| SUGGESTIONS:".ljust(67) + "|")
|
|
171
|
+
lines.append("|".ljust(67) + "|")
|
|
172
172
|
|
|
173
173
|
for i, suggestion in enumerate(suggestions, 1):
|
|
174
174
|
for line in f"{i}. {suggestion}".split('\n'):
|
|
175
|
-
lines.append(f"
|
|
176
|
-
lines.append("
|
|
175
|
+
lines.append(f"| {line}".ljust(67) + "|")
|
|
176
|
+
lines.append("|".ljust(67) + "|")
|
|
177
177
|
|
|
178
178
|
return lines
|
|
179
179
|
|
|
@@ -185,13 +185,19 @@ class DiagnosticReport:
|
|
|
185
185
|
}, indent=2, default=str)
|
|
186
186
|
|
|
187
187
|
|
|
188
|
-
def generate_report(
|
|
189
|
-
"""Generate a
|
|
190
|
-
|
|
188
|
+
def generate_report(source: Any) -> DiagnosticReport:
|
|
189
|
+
"""Generate a report from an engine or a diagnostics dictionary."""
|
|
190
|
+
if isinstance(source, dict):
|
|
191
|
+
diagnostics = source.get('diagnostics', source)
|
|
192
|
+
elif hasattr(source, 'get_diagnostics'):
|
|
193
|
+
diagnostics = source.get_diagnostics()
|
|
194
|
+
elif hasattr(source, '_engine'):
|
|
195
|
+
diagnostics = source._engine.get_diagnostics()
|
|
196
|
+
else:
|
|
197
|
+
raise TypeError("expected diagnostics dict, engine, or stabilized driver")
|
|
191
198
|
return DiagnosticReport(diagnostics)
|
|
192
199
|
|
|
193
200
|
|
|
194
|
-
def print_report(
|
|
195
|
-
"""Print a
|
|
196
|
-
|
|
197
|
-
print(report.generate_text_report())
|
|
201
|
+
def print_report(source: Any) -> None:
|
|
202
|
+
"""Print a report from diagnostics, an engine, or a stabilized driver."""
|
|
203
|
+
print(generate_report(source).generate_text_report())
|