waitless 0.3.1__tar.gz → 1.0.0__tar.gz

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