waitless 0.3.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waitless
3
- Version: 0.3.0
3
+ Version: 0.3.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
6
  License: MIT
@@ -84,7 +84,7 @@ Automation tests fail because interactions happen while the UI is still changing
84
84
 
85
85
  Waitless monitors the **entire page** for stability signals:
86
86
 
87
- - ✅ DOM mutation activity (MutationObserver)
87
+ - ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
88
88
  - ✅ Pending network requests (XHR/fetch interception)
89
89
  - ✅ CSS animations and transitions
90
90
  - ✅ Layout stability (element movement)
@@ -171,18 +171,18 @@ python -m waitless doctor --file diagnostics.json
171
171
 
172
172
  Sample output:
173
173
  ```
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
- ╚══════════════════════════════════════════════════════════════════╝
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
+ +--------------------------------------------------------------------+
186
186
  ```
187
187
 
188
188
  ## Important Notes
@@ -214,12 +214,11 @@ element = driver.find_element(By.ID, "button")
214
214
  original = element.unwrap() # Gets the real WebElement
215
215
  ```
216
216
 
217
- ## v0.3 Limitations
217
+ ## v0.3.2 Limitations
218
218
 
219
219
  - **Selenium only** - Playwright support planned for v1
220
220
  - **Sync only** - No async/await support yet
221
221
  - **Main frame only** - iframes not monitored
222
- - **No Shadow DOM** - MutationObserver doesn't see shadow roots
223
222
  - **No Service Workers** - SW network requests not intercepted
224
223
 
225
224
  See [CHANGELOG.md](CHANGELOG.md) for version history.
@@ -54,7 +54,7 @@ Automation tests fail because interactions happen while the UI is still changing
54
54
 
55
55
  Waitless monitors the **entire page** for stability signals:
56
56
 
57
- - ✅ DOM mutation activity (MutationObserver)
57
+ - ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
58
58
  - ✅ Pending network requests (XHR/fetch interception)
59
59
  - ✅ CSS animations and transitions
60
60
  - ✅ Layout stability (element movement)
@@ -141,18 +141,18 @@ python -m waitless doctor --file diagnostics.json
141
141
 
142
142
  Sample output:
143
143
  ```
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
- ╚══════════════════════════════════════════════════════════════════╝
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
+ +--------------------------------------------------------------------+
156
156
  ```
157
157
 
158
158
  ## Important Notes
@@ -184,12 +184,11 @@ element = driver.find_element(By.ID, "button")
184
184
  original = element.unwrap() # Gets the real WebElement
185
185
  ```
186
186
 
187
- ## v0.3 Limitations
187
+ ## v0.3.2 Limitations
188
188
 
189
189
  - **Selenium only** - Playwright support planned for v1
190
190
  - **Sync only** - No async/await support yet
191
191
  - **Main frame only** - iframes not monitored
192
- - **No Shadow DOM** - MutationObserver doesn't see shadow roots
193
192
  - **No Service Workers** - SW network requests not intercepted
194
193
 
195
194
  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.0"
7
+ version = "0.3.2"
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.0'
39
+ __version__ = '0.3.2'
40
40
  __author__ = 'Dhiraj Das'
41
41
 
42
42
  # Public API
@@ -71,34 +71,34 @@ 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".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 + "-+")
102
102
  return 0
103
103
 
104
104
  # Generate report
@@ -24,71 +24,71 @@ class DiagnosticReport:
24
24
  def generate_text_report(self) -> str:
25
25
  """Generate a text-based diagnostic report."""
26
26
  lines = []
27
- lines.append("" + "" * 66 + "")
28
- lines.append("" + "WAITLESS STABILITY REPORT".center(66) + "")
29
- lines.append("" + "" * 66 + "")
30
- lines.append(f" Report generated at: {self.timestamp.strftime('%Y-%m-%d %H:%M:%S'):<43} ")
27
+ lines.append("+-" + "-" * 66 + "-+")
28
+ lines.append("|" + "WAITLESS STABILITY REPORT".center(66) + "|")
29
+ lines.append("+-" + "-" * 66 + "-+")
30
+ lines.append(f"| Report generated at: {self.timestamp.strftime('%Y-%m-%d %H:%M:%S'):<43} |")
31
31
 
32
32
  config = self.diagnostics.get('config', {})
33
- lines.append("" + "" * 66 + "")
34
- lines.append(" CONFIGURATION:".ljust(67) + "")
35
- lines.append(f" Timeout: {config.get('timeout', 'N/A')}s".ljust(67) + "")
36
- lines.append(f" Strictness: {config.get('strictness', 'N/A')}".ljust(67) + "")
37
- lines.append(f" Network threshold: {config.get('network_idle_threshold', 'N/A')} pending requests".ljust(67) + "")
38
- lines.append(f" Animation detection: {config.get('animation_detection', 'N/A')}".ljust(67) + "")
33
+ lines.append("+-" + "-" * 66 + "-+")
34
+ lines.append("| CONFIGURATION:".ljust(67) + "|")
35
+ lines.append(f"| Timeout: {config.get('timeout', 'N/A')}s".ljust(67) + "|")
36
+ lines.append(f"| Strictness: {config.get('strictness', 'N/A')}".ljust(67) + "|")
37
+ lines.append(f"| Network threshold: {config.get('network_idle_threshold', 'N/A')} pending requests".ljust(67) + "|")
38
+ lines.append(f"| Animation detection: {config.get('animation_detection', 'N/A')}".ljust(67) + "|")
39
39
 
40
40
  blocking = self.diagnostics.get('blocking_factors', {})
41
41
  if blocking:
42
- lines.append("" + "" * 66 + "")
43
- lines.append(" BLOCKING FACTORS:".ljust(67) + "")
44
- lines.append("".ljust(67) + "")
42
+ lines.append("+-" + "-" * 66 + "-+")
43
+ lines.append("| BLOCKING FACTORS:".ljust(67) + "|")
44
+ lines.append("|".ljust(67) + "|")
45
45
 
46
46
  pending = blocking.get('pending_requests', 0)
47
47
  if pending > 0:
48
- lines.append(" NETWORK: {} request(s) still pending".format(pending).ljust(67) + "")
48
+ lines.append("| [!] NETWORK: {} request(s) still pending".format(pending).ljust(67) + "|")
49
49
 
50
50
  details = blocking.get('pending_request_details', [])
51
51
  for req in details[:5]: # Show max 5
52
52
  url = req.get('url', 'unknown')[:50]
53
53
  started = req.get('startTime', 0)
54
- lines.append(f" {req.get('type', 'unknown').upper()} {url}".ljust(67) + "")
54
+ lines.append(f"| -> {req.get('type', 'unknown').upper()} {url}".ljust(67) + "|")
55
55
 
56
56
  if len(details) > 5:
57
- lines.append(f" ... and {len(details) - 5} more".ljust(67) + "")
58
- lines.append("".ljust(67) + "")
57
+ lines.append(f"| ... and {len(details) - 5} more".ljust(67) + "|")
58
+ lines.append("|".ljust(67) + "|")
59
59
 
60
60
  animations = blocking.get('active_animations', 0)
61
61
  if animations > 0:
62
- lines.append(f" ANIMATIONS: {animations} active animation(s)".ljust(67) + "")
63
- lines.append("".ljust(67) + "")
62
+ lines.append(f"| [!] ANIMATIONS: {animations} active animation(s)".ljust(67) + "|")
63
+ lines.append("|".ljust(67) + "|")
64
64
 
65
65
  if blocking.get('layout_shifting'):
66
- lines.append(" LAYOUT: Elements are still moving".ljust(67) + "")
67
- lines.append("".ljust(67) + "")
66
+ lines.append("| [!] LAYOUT: Elements are still moving".ljust(67) + "|")
67
+ lines.append("|".ljust(67) + "|")
68
68
 
69
69
  status = self.diagnostics.get('last_status')
70
70
  if status:
71
- lines.append("" + "" * 66 + "")
72
- lines.append(" SIGNAL STATUS:".ljust(67) + "")
71
+ lines.append("+-" + "-" * 66 + "-+")
72
+ lines.append("| SIGNAL STATUS:".ljust(67) + "|")
73
73
 
74
74
  for signal in status.get('signals', []):
75
- state = "" if signal['state'] == 'STABLE' else ""
75
+ state = "[OK]" if signal['state'] == 'STABLE' else "[WAIT]"
76
76
  mandatory = "[M]" if signal['mandatory'] else "[O]"
77
- line = f" {state} {mandatory} {signal['type']}: {signal.get('details', 'N/A')}"
78
- lines.append(line[:66].ljust(67) + "")
77
+ line = f"| {state} {mandatory} {signal['type']}: {signal.get('details', 'N/A')}"
78
+ lines.append(line[:66].ljust(67) + "|")
79
79
 
80
80
  timeline = self.diagnostics.get('timeline', [])
81
81
  if timeline:
82
- lines.append("" + "" * 66 + "")
83
- lines.append(" RECENT EVENTS (last 10):".ljust(67) + "")
82
+ lines.append("+-" + "-" * 66 + "-+")
83
+ lines.append("| RECENT EVENTS (last 10):".ljust(67) + "|")
84
84
 
85
85
  for entry in timeline[-10:]:
86
86
  time_str = str(entry.get('time', ''))[-6:]
87
87
  msg = entry.get('message', '')[:50]
88
- lines.append(f" [{time_str}] {msg}".ljust(67) + "")
88
+ lines.append(f"| [{time_str}] {msg}".ljust(67) + "|")
89
89
 
90
90
  lines.extend(self._generate_suggestions())
91
- lines.append("" + "" * 66 + "")
91
+ lines.append("+-" + "-" * 66 + "-+")
92
92
 
93
93
  return "\n".join(lines)
94
94
 
@@ -71,6 +71,7 @@ INSTRUMENTATION_SCRIPT = """
71
71
  // Rolling window for mutation rate calculation
72
72
  _mutationTimestamps: [],
73
73
  _mutationWindowMs: 1000, // 1 second window for rate calculation
74
+ _observedShadowRoots: new WeakSet(),
74
75
 
75
76
  _setupMutationObserver: function() {
76
77
  var self = this;
@@ -87,17 +88,77 @@ INSTRUMENTATION_SCRIPT = """
87
88
  self._mutationTimestamps.shift();
88
89
  }
89
90
 
91
+ // Check for new shadow roots in added nodes
92
+ mutations.forEach(function(mutation) {
93
+ mutation.addedNodes.forEach(function(node) {
94
+ if (node.nodeType === 1) { // Element node
95
+ self._observeShadowRoots(node);
96
+ }
97
+ });
98
+ });
99
+
90
100
  self._log('DOM mutation', { count: mutations.length, rate: self.getMutationRate() });
91
101
  });
92
102
 
93
- observer.observe(document.documentElement || document.body, {
103
+ var config = {
94
104
  childList: true,
95
105
  subtree: true,
96
106
  attributes: true,
97
107
  characterData: true
98
- });
108
+ };
99
109
 
110
+ observer.observe(document.documentElement || document.body, config);
100
111
  this._observers.push(observer);
112
+
113
+ // Initial scan for shadow roots
114
+ this._observeShadowRoots(document);
115
+ },
116
+
117
+ _observeShadowRoots: function(root) {
118
+ var self = this;
119
+
120
+ // Function to recursively find and observe shadow roots
121
+ var walk = function(node) {
122
+ if (node.shadowRoot && !self._observedShadowRoots.has(node.shadowRoot)) {
123
+ self._observedShadowRoots.add(node.shadowRoot);
124
+
125
+ var observer = new MutationObserver(function(mutations) {
126
+ var now = Date.now();
127
+ self.lastMutationTime = now;
128
+ self._mutationTimestamps.push(now);
129
+ self._log('Shadow DOM mutation', { count: mutations.length });
130
+
131
+ // Scan new nodes in shadow DOM for nested shadow roots
132
+ mutations.forEach(function(mutation) {
133
+ mutation.addedNodes.forEach(function(newNode) {
134
+ if (newNode.nodeType === 1) walk(newNode);
135
+ });
136
+ });
137
+ });
138
+
139
+ observer.observe(node.shadowRoot, {
140
+ childList: true,
141
+ subtree: true,
142
+ attributes: true,
143
+ characterData: true
144
+ });
145
+
146
+ self._observers.push(observer);
147
+ self._log('Observing shadow root', { host: node.tagName });
148
+
149
+ // Recurse into the shadow root
150
+ walk(node.shadowRoot);
151
+ }
152
+
153
+ // Traverse children
154
+ var child = node.firstElementChild;
155
+ while (child) {
156
+ walk(child);
157
+ child = child.nextElementSibling;
158
+ }
159
+ };
160
+
161
+ walk(root);
101
162
  },
102
163
 
103
164
  // Calculate mutations per second from rolling window
@@ -239,8 +300,26 @@ INSTRUMENTATION_SCRIPT = """
239
300
  },
240
301
 
241
302
  _checkLayoutStability: function() {
242
- // Track key interactive elements
243
- var elements = document.querySelectorAll('button, a, input, [onclick], [role="button"]');
303
+ // Track key interactive elements, including those in shadow DOM
304
+ var elements = [];
305
+
306
+ var collectElements = function(root) {
307
+ var found = root.querySelectorAll('button, a, input, [onclick], [role="button"]');
308
+ for (var i = 0; i < found.length; i++) {
309
+ elements.push(found[i]);
310
+ }
311
+
312
+ // Recursively check shadow roots
313
+ var all = root.querySelectorAll('*');
314
+ for (var j = 0; j < all.length; j++) {
315
+ if (all[j].shadowRoot) {
316
+ collectElements(all[j].shadowRoot);
317
+ }
318
+ }
319
+ };
320
+
321
+ collectElements(document);
322
+
244
323
  var isShifting = false;
245
324
  var self = this;
246
325
 
@@ -170,13 +170,16 @@ class SignalEvaluator:
170
170
  if mutation_rate is not None:
171
171
  threshold = self.config.mutation_rate_threshold
172
172
  is_stable = mutation_rate <= threshold
173
+
174
+ details = f"Mutation rate: {mutation_rate:.0f}/sec (threshold: {threshold:.0f}/sec)"
175
+
173
176
  return Signal(
174
177
  signal_type=SignalType.DOM_MUTATIONS,
175
178
  state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
176
179
  value=mutation_rate,
177
180
  threshold=threshold,
178
181
  is_mandatory=True,
179
- details=f"Mutation rate: {mutation_rate:.0f}/sec (threshold: {threshold:.0f}/sec)",
182
+ details=details,
180
183
  )
181
184
 
182
185
  # Fallback: Use time since last mutation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waitless
3
- Version: 0.3.0
3
+ Version: 0.3.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
6
  License: MIT
@@ -84,7 +84,7 @@ Automation tests fail because interactions happen while the UI is still changing
84
84
 
85
85
  Waitless monitors the **entire page** for stability signals:
86
86
 
87
- - ✅ DOM mutation activity (MutationObserver)
87
+ - ✅ DOM mutation activity (MutationObserver, including **Shadow DOM**)
88
88
  - ✅ Pending network requests (XHR/fetch interception)
89
89
  - ✅ CSS animations and transitions
90
90
  - ✅ Layout stability (element movement)
@@ -171,18 +171,18 @@ python -m waitless doctor --file diagnostics.json
171
171
 
172
172
  Sample output:
173
173
  ```
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
- ╚══════════════════════════════════════════════════════════════════╝
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
+ +--------------------------------------------------------------------+
186
186
  ```
187
187
 
188
188
  ## Important Notes
@@ -214,12 +214,11 @@ element = driver.find_element(By.ID, "button")
214
214
  original = element.unwrap() # Gets the real WebElement
215
215
  ```
216
216
 
217
- ## v0.3 Limitations
217
+ ## v0.3.2 Limitations
218
218
 
219
219
  - **Selenium only** - Playwright support planned for v1
220
220
  - **Sync only** - No async/await support yet
221
221
  - **Main frame only** - iframes not monitored
222
- - **No Shadow DOM** - MutationObserver doesn't see shadow roots
223
222
  - **No Service Workers** - SW network requests not intercepted
224
223
 
225
224
  See [CHANGELOG.md](CHANGELOG.md) for version history.
File without changes
File without changes
File without changes
File without changes