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.
Files changed (25) hide show
  1. {waitless-0.3.2/waitless.egg-info → waitless-1.0.1}/PKG-INFO +56 -10
  2. {waitless-0.3.2 → waitless-1.0.1}/README.md +52 -6
  3. {waitless-0.3.2 → waitless-1.0.1}/pyproject.toml +6 -4
  4. {waitless-0.3.2 → waitless-1.0.1}/waitless/__init__.py +1 -1
  5. {waitless-0.3.2 → waitless-1.0.1}/waitless/__main__.py +8 -2
  6. waitless-1.0.1/waitless/adapters/__init__.py +20 -0
  7. waitless-1.0.1/waitless/adapters/angular.py +137 -0
  8. waitless-1.0.1/waitless/adapters/base.py +79 -0
  9. waitless-1.0.1/waitless/adapters/react.py +140 -0
  10. waitless-1.0.1/waitless/adapters/vue.py +156 -0
  11. {waitless-0.3.2 → waitless-1.0.1}/waitless/config.py +50 -1
  12. {waitless-0.3.2 → waitless-1.0.1}/waitless/diagnostics.py +44 -12
  13. {waitless-0.3.2 → waitless-1.0.1}/waitless/engine.py +141 -14
  14. {waitless-0.3.2 → waitless-1.0.1}/waitless/instrumentation.py +257 -23
  15. {waitless-0.3.2 → waitless-1.0.1}/waitless/selenium_integration.py +45 -53
  16. {waitless-0.3.2 → waitless-1.0.1}/waitless/signals.py +98 -2
  17. {waitless-0.3.2 → waitless-1.0.1/waitless.egg-info}/PKG-INFO +56 -10
  18. {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/SOURCES.txt +6 -1
  19. {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/requires.txt +2 -1
  20. {waitless-0.3.2 → waitless-1.0.1}/LICENSE +0 -0
  21. {waitless-0.3.2 → waitless-1.0.1}/setup.cfg +0 -0
  22. {waitless-0.3.2 → waitless-1.0.1}/waitless/exceptions.py +0 -0
  23. {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/dependency_links.txt +0 -0
  24. {waitless-0.3.2 → waitless-1.0.1}/waitless.egg-info/entry_points.txt +0 -0
  25. {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.2
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: 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
+
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(engine) # Print detailed 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
- ## v0.3.2 Limitations
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 for v1
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
  [![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
+
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(engine) # Print detailed 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
- ## v0.3.2 Limitations
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 for v1
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.3.2"
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 = {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,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
- "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__ = '0.3.2'
39
+ __version__ = '1.0.1'
40
40
  __author__ = 'Dhiraj Das'
41
41
 
42
42
  # Public API
@@ -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("| print_report(engine) # Print diagnostic report".ljust(67) + "|")
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
+ """