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.
Files changed (25) hide show
  1. {waitless-1.0.0/waitless.egg-info → waitless-1.0.2}/PKG-INFO +28 -16
  2. {waitless-1.0.0 → waitless-1.0.2}/README.md +21 -9
  3. {waitless-1.0.0 → waitless-1.0.2}/pyproject.toml +9 -7
  4. {waitless-1.0.0 → waitless-1.0.2}/waitless/__init__.py +1 -1
  5. {waitless-1.0.0 → waitless-1.0.2}/waitless/__main__.py +2 -1
  6. {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/angular.py +41 -13
  7. {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/base.py +6 -0
  8. {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/react.py +40 -11
  9. {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/vue.py +59 -20
  10. {waitless-1.0.0 → waitless-1.0.2}/waitless/config.py +23 -0
  11. {waitless-1.0.0 → waitless-1.0.2}/waitless/diagnostics.py +18 -12
  12. {waitless-1.0.0 → waitless-1.0.2}/waitless/engine.py +141 -14
  13. {waitless-1.0.0 → waitless-1.0.2}/waitless/instrumentation.py +35 -23
  14. {waitless-1.0.0 → waitless-1.0.2}/waitless/selenium_integration.py +45 -53
  15. {waitless-1.0.0 → waitless-1.0.2}/waitless/signals.py +37 -2
  16. {waitless-1.0.0 → waitless-1.0.2/waitless.egg-info}/PKG-INFO +28 -16
  17. {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/requires.txt +2 -1
  18. {waitless-1.0.0 → waitless-1.0.2}/LICENSE +0 -0
  19. {waitless-1.0.0 → waitless-1.0.2}/setup.cfg +0 -0
  20. {waitless-1.0.0 → waitless-1.0.2}/waitless/adapters/__init__.py +0 -0
  21. {waitless-1.0.0 → waitless-1.0.2}/waitless/exceptions.py +0 -0
  22. {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/SOURCES.txt +0 -0
  23. {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/dependency_links.txt +0 -0
  24. {waitless-1.0.0 → waitless-1.0.2}/waitless.egg-info/entry_points.txt +0 -0
  25. {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.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://github.com/godhiraj-code/waitless#readme
9
- Project-URL: Repository, https://github.com/godhiraj-code/waitless.git
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: selenium>=4.0; extra == "dev"
28
+ Requires-Dist: build>=1.0; extra == "dev"
29
29
  Dynamic: license-file
30
30
 
31
31
  # Waitless
32
32
 
33
33
  [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
34
34
 
35
- **Zero-wait UI automation stabilization for Selenium**
35
+ **Automatic UI stabilization for Selenium**
36
36
 
37
- Eliminate explicit waits and sleeps by automatically detecting true UI stability.
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
+ ![Waitless stability demo](examples/waitless_demo.webp)
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 monitoring (opt-in)
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(engine) # Print detailed 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 | ~5-10ms |
244
- | Per-poll overhead | ~1-2ms |
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
- ### SPA Navigation Handling
250
+ ### Navigation Handling
249
251
 
250
- Waitless automatically re-injects instrumentation after SPA route changes:
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 works transparently with React Router, Vue Router, Angular Router, etc.
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
  [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml)
4
4
 
5
- **Zero-wait UI automation stabilization for Selenium**
5
+ **Automatic UI stabilization for Selenium**
6
6
 
7
- Eliminate explicit waits and sleeps by automatically detecting true UI stability.
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
+ ![Waitless stability demo](examples/waitless_demo.webp)
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 monitoring (opt-in)
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(engine) # Print detailed 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 | ~5-10ms |
214
- | Per-poll overhead | ~1-2ms |
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
- ### SPA Navigation Handling
220
+ ### Navigation Handling
219
221
 
220
- Waitless automatically re-injects instrumentation after SPA route changes:
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 works transparently with React Router, Vue Router, Angular Router, etc.
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.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 = {text = "MIT"}
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://github.com/godhiraj-code/waitless#readme"
41
- Repository = "https://github.com/godhiraj-code/waitless.git"
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
- "selenium>=4.0",
52
+ "build>=1.0",
51
53
  ]
52
54
 
53
55
  [tool.setuptools.packages.find]
@@ -36,7 +36,7 @@ Disable:
36
36
  driver = unstabilize(driver) # Back to original behavior
37
37
  """
38
38
 
39
- __version__ = '1.0.0'
39
+ __version__ = '1.0.2'
40
40
  __author__ = 'Dhiraj Das'
41
41
 
42
42
  # Public API
@@ -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("| print_report(engine) # Print diagnostic report".ljust(67) + "|")
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.isStable = true;
60
- window.__waitless__.framework.angular.lastStableTime = Date.now();
61
- window.__waitless__._log('Angular became stable');
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
- if (this.name === 'angular') {
71
- window.__waitless__.framework.angular.isStable = false;
72
- window.__waitless__.framework.angular.pendingTasks++;
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
- 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();
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.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
-
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.pendingUpdates++;
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.pendingUpdates--;
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
- hook.on && hook.on('component:updated', function() {
63
- window.__waitless__.framework.vue.lastUpdateTime = Date.now();
64
- window.__waitless__.framework.vue.isSettled = false;
65
- window.__waitless__._log('Vue component updated');
66
-
67
- setTimeout(function() {
68
- window.__waitless__.framework.vue.isSettled = true;
69
- }, 50);
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.pendingTicks++;
77
- window.__waitless__.framework.vue.isSettled = false;
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.pendingTicks--;
81
- window.__waitless__.framework.vue.lastUpdateTime = Date.now();
82
- if (window.__waitless__.framework.vue.pendingTicks === 0) {
83
- window.__waitless__.framework.vue.isSettled = true;
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("╠" + "═" * 66 + "╣")
170
- lines.append("║ SUGGESTIONS:".ljust(67) + "║")
171
- lines.append("║".ljust(67) + "║")
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"║ {line}".ljust(67) + "║")
176
- lines.append("║".ljust(67) + "║")
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(engine: 'StabilizationEngine') -> DiagnosticReport:
189
- """Generate a diagnostic report from an engine."""
190
- diagnostics = engine.get_diagnostics()
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(engine: 'StabilizationEngine') -> None:
195
- """Print a diagnostic report to stdout."""
196
- report = generate_report(engine)
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())