scitex 2.4.2__py3-none-any.whl → 2.4.3__py3-none-any.whl

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.
scitex/__version__.py CHANGED
@@ -9,6 +9,6 @@ __FILE__ = "./src/scitex/__version__.py"
9
9
  __DIR__ = os.path.dirname(__FILE__)
10
10
  # ----------------------------------------
11
11
 
12
- __version__ = "2.4.2"
12
+ __version__ = "2.4.3"
13
13
 
14
14
  # EOF
@@ -8,6 +8,32 @@ from .debugging import (
8
8
  browser_logger,
9
9
  show_grid_async,
10
10
  highlight_element_async,
11
+ # Visual cursor/feedback utilities (sync and async)
12
+ inject_visual_effects,
13
+ inject_visual_effects_async,
14
+ show_cursor_at,
15
+ show_cursor_at_async,
16
+ show_click_effect,
17
+ show_click_effect_async,
18
+ show_step,
19
+ show_step_async,
20
+ show_test_result,
21
+ show_test_result_async,
22
+ # Failure capture utilities (mirrors console-interceptor.ts)
23
+ setup_console_interceptor,
24
+ collect_console_logs,
25
+ collect_console_logs_detailed,
26
+ format_logs_devtools_style,
27
+ save_failure_artifacts,
28
+ create_failure_capture_fixture,
29
+ # Test monitoring (periodic screenshots via scitex.capture)
30
+ TestMonitor,
31
+ create_test_monitor_fixture,
32
+ monitor_test,
33
+ # Sync browser session for zombie prevention
34
+ SyncBrowserSession,
35
+ sync_browser_session,
36
+ create_browser_session_fixture,
11
37
  )
12
38
 
13
39
  # PDF utilities
@@ -31,6 +57,33 @@ __all__ = [
31
57
  "browser_logger",
32
58
  "show_grid_async",
33
59
  "highlight_element_async",
60
+ # Visual cursor/feedback (sync)
61
+ "inject_visual_effects",
62
+ "show_cursor_at",
63
+ "show_click_effect",
64
+ "show_step",
65
+ "show_test_result",
66
+ # Visual cursor/feedback (async)
67
+ "inject_visual_effects_async",
68
+ "show_cursor_at_async",
69
+ "show_click_effect_async",
70
+ "show_step_async",
71
+ "show_test_result_async",
72
+ # Failure capture utilities (mirrors console-interceptor.ts)
73
+ "setup_console_interceptor",
74
+ "collect_console_logs",
75
+ "collect_console_logs_detailed",
76
+ "format_logs_devtools_style",
77
+ "save_failure_artifacts",
78
+ "create_failure_capture_fixture",
79
+ # Test monitoring (periodic screenshots via scitex.capture)
80
+ "TestMonitor",
81
+ "create_test_monitor_fixture",
82
+ "monitor_test",
83
+ # Sync browser session for zombie prevention
84
+ "SyncBrowserSession",
85
+ "sync_browser_session",
86
+ "create_browser_session_fixture",
34
87
 
35
88
  # PDF
36
89
  "detect_chrome_pdf_viewer_async",
@@ -7,12 +7,68 @@
7
7
  from ._browser_logger import browser_logger
8
8
  from ._show_grid import show_grid_async
9
9
  from ._highlight_element import highlight_element_async
10
+ from ._visual_cursor import (
11
+ inject_visual_effects,
12
+ inject_visual_effects_async,
13
+ show_cursor_at,
14
+ show_cursor_at_async,
15
+ show_click_effect,
16
+ show_click_effect_async,
17
+ show_step,
18
+ show_step_async,
19
+ show_test_result,
20
+ show_test_result_async,
21
+ )
22
+ from ._failure_capture import (
23
+ setup_console_interceptor,
24
+ collect_console_logs,
25
+ collect_console_logs_detailed,
26
+ format_logs_devtools_style,
27
+ save_failure_artifacts,
28
+ create_failure_capture_fixture,
29
+ )
30
+ from ._test_monitor import (
31
+ TestMonitor,
32
+ create_test_monitor_fixture,
33
+ monitor_test,
34
+ )
35
+ from ._sync_session import (
36
+ SyncBrowserSession,
37
+ sync_browser_session,
38
+ create_browser_session_fixture,
39
+ )
10
40
 
11
41
  __all__ = [
12
42
  "log_page_async",
13
43
  "browser_logger",
14
44
  "show_grid_async",
15
45
  "highlight_element_async",
46
+ # Visual cursor/feedback utilities
47
+ "inject_visual_effects",
48
+ "inject_visual_effects_async",
49
+ "show_cursor_at",
50
+ "show_cursor_at_async",
51
+ "show_click_effect",
52
+ "show_click_effect_async",
53
+ "show_step",
54
+ "show_step_async",
55
+ "show_test_result",
56
+ "show_test_result_async",
57
+ # Failure capture utilities
58
+ "setup_console_interceptor",
59
+ "collect_console_logs",
60
+ "collect_console_logs_detailed",
61
+ "format_logs_devtools_style",
62
+ "save_failure_artifacts",
63
+ "create_failure_capture_fixture",
64
+ # Test monitoring (periodic screenshots via scitex.capture)
65
+ "TestMonitor",
66
+ "create_test_monitor_fixture",
67
+ "monitor_test",
68
+ # Sync browser session for zombie prevention
69
+ "SyncBrowserSession",
70
+ "sync_browser_session",
71
+ "create_browser_session_fixture",
16
72
  ]
17
73
 
18
74
  # EOF
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-08
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/browser/debugging/_failure_capture.py
5
+
6
+ """
7
+ Automatic failure capture utilities for Playwright E2E tests.
8
+
9
+ Features:
10
+ - Console log collection with source file/line tracking
11
+ - Error interception (JS errors, unhandled promise rejections, resource failures)
12
+ - Screenshot capture on test failure
13
+ - Page HTML capture for debugging
14
+ - DevTools-like formatted output
15
+ - Pytest integration via fixtures
16
+
17
+ Based on scitex-cloud's console-interceptor.ts functionality.
18
+
19
+ Usage in conftest.py:
20
+ from scitex.browser.debugging import (
21
+ setup_console_interceptor,
22
+ collect_console_logs,
23
+ save_failure_artifacts,
24
+ create_failure_capture_fixture,
25
+ )
26
+ """
27
+
28
+ from datetime import datetime
29
+ from pathlib import Path
30
+ from typing import TYPE_CHECKING
31
+
32
+ if TYPE_CHECKING:
33
+ from playwright.sync_api import Page
34
+
35
+
36
+ # JavaScript code for advanced console interception
37
+ # Mirrors functionality from scitex-cloud/static/shared/ts/utils/console-interceptor.ts
38
+ CONSOLE_INTERCEPTOR_JS = """
39
+ () => {
40
+ if (window._scitex_console_interceptor_setup) return;
41
+
42
+ // Store for captured logs with full details
43
+ window._scitex_console_logs = [];
44
+ window._scitex_console_history = [];
45
+ const maxHistory = 2000;
46
+
47
+ // Store original console methods
48
+ const originalConsole = {
49
+ log: console.log,
50
+ info: console.info,
51
+ warn: console.warn,
52
+ error: console.error,
53
+ debug: console.debug
54
+ };
55
+
56
+ // Get source file and line number from stack trace
57
+ function getSource() {
58
+ try {
59
+ const stack = new Error().stack;
60
+ if (!stack) return '';
61
+ const lines = stack.split('\\n');
62
+ // Skip Error, getSource, capture, and intercepted console method
63
+ for (let i = 4; i < lines.length; i++) {
64
+ const line = lines[i];
65
+ const match = line.match(/(?:https?:\\/\\/[^\\/]+)?([^\\s]+):(\\d+):(\\d+)/);
66
+ if (match) {
67
+ const [, file, lineNum, col] = match;
68
+ const cleanFile = file.split('/').slice(-2).join('/');
69
+ return `${cleanFile}:${lineNum}:${col}`;
70
+ }
71
+ }
72
+ } catch (e) {}
73
+ return '';
74
+ }
75
+
76
+ // Format message from arguments
77
+ function formatMessage(args) {
78
+ return args.map(arg => {
79
+ if (typeof arg === 'object') {
80
+ try { return JSON.stringify(arg, null, 2); }
81
+ catch { return String(arg); }
82
+ }
83
+ return String(arg);
84
+ }).join(' ');
85
+ }
86
+
87
+ // Capture log entry
88
+ function capture(level, args) {
89
+ const message = formatMessage(args);
90
+ const source = getSource();
91
+ const entry = {
92
+ level,
93
+ message,
94
+ source,
95
+ timestamp: Date.now(),
96
+ url: window.location.href
97
+ };
98
+
99
+ window._scitex_console_history.push(entry);
100
+ if (window._scitex_console_history.length > maxHistory) {
101
+ window._scitex_console_history.shift();
102
+ }
103
+
104
+ // Also store simple format for backwards compatibility
105
+ window._scitex_console_logs.push(`[${level.toUpperCase()}] ${source ? source + ' ' : ''}${message}`);
106
+ if (window._scitex_console_logs.length > 500) {
107
+ window._scitex_console_logs.shift();
108
+ }
109
+ }
110
+
111
+ // Intercept console methods
112
+ ['log', 'info', 'warn', 'error', 'debug'].forEach(level => {
113
+ console[level] = function(...args) {
114
+ originalConsole[level].apply(console, args);
115
+ capture(level, args);
116
+ };
117
+ });
118
+
119
+ // Capture unhandled JavaScript errors
120
+ window.addEventListener('error', (event) => {
121
+ let entry;
122
+ if (event.target && event.target.tagName) {
123
+ // Resource loading error
124
+ const target = event.target;
125
+ const src = target.src || target.href || '';
126
+ if (src) {
127
+ entry = {
128
+ level: 'error',
129
+ message: `Failed to load resource: ${src}`,
130
+ source: src.split('/').pop() || '',
131
+ timestamp: Date.now(),
132
+ url: window.location.href
133
+ };
134
+ }
135
+ } else {
136
+ // JavaScript error
137
+ entry = {
138
+ level: 'error',
139
+ message: event.message,
140
+ source: `${event.filename}:${event.lineno}:${event.colno}`,
141
+ timestamp: Date.now(),
142
+ url: window.location.href
143
+ };
144
+ }
145
+ if (entry) {
146
+ window._scitex_console_history.push(entry);
147
+ window._scitex_console_logs.push(`[ERROR] ${entry.source} ${entry.message}`);
148
+ }
149
+ }, true);
150
+
151
+ // Capture unhandled promise rejections
152
+ window.addEventListener('unhandledrejection', (event) => {
153
+ const entry = {
154
+ level: 'error',
155
+ message: `Uncaught (in promise): ${event.reason}`,
156
+ source: '',
157
+ timestamp: Date.now(),
158
+ url: window.location.href
159
+ };
160
+ window._scitex_console_history.push(entry);
161
+ window._scitex_console_logs.push(`[ERROR] Uncaught (in promise): ${event.reason}`);
162
+ });
163
+
164
+ window._scitex_console_interceptor_setup = true;
165
+ }
166
+ """
167
+
168
+
169
+ def setup_console_interceptor(page: "Page") -> None:
170
+ """Set up console log interceptor with source tracking and error capture.
171
+
172
+ Features (mirroring console-interceptor.ts):
173
+ - Intercepts console.log, info, warn, error, debug
174
+ - Captures source file and line number
175
+ - Captures unhandled JS errors
176
+ - Captures unhandled promise rejections
177
+ - Captures resource loading failures
178
+
179
+ Call this at the start of each test to begin capturing logs.
180
+ """
181
+ try:
182
+ page.evaluate(CONSOLE_INTERCEPTOR_JS)
183
+ except Exception:
184
+ pass
185
+
186
+
187
+ def collect_console_logs(page: "Page") -> list:
188
+ """Collect all captured console logs from the browser.
189
+
190
+ Returns:
191
+ List of log strings in format "[LEVEL] source message"
192
+ """
193
+ try:
194
+ logs = page.evaluate("""
195
+ () => {
196
+ if (window._scitex_console_logs) {
197
+ return window._scitex_console_logs;
198
+ }
199
+ return [];
200
+ }
201
+ """)
202
+ return logs or []
203
+ except Exception:
204
+ return []
205
+
206
+
207
+ def collect_console_logs_detailed(page: "Page") -> list:
208
+ """Collect all captured console logs with full details.
209
+
210
+ Returns:
211
+ List of dicts with keys: level, message, source, timestamp, url
212
+ """
213
+ try:
214
+ history = page.evaluate("""
215
+ () => {
216
+ if (window._scitex_console_history) {
217
+ return window._scitex_console_history;
218
+ }
219
+ return [];
220
+ }
221
+ """)
222
+ return history or []
223
+ except Exception:
224
+ return []
225
+
226
+
227
+ def format_logs_devtools_style(logs: list) -> str:
228
+ """Format logs in DevTools-like style.
229
+
230
+ Args:
231
+ logs: List of detailed log entries from collect_console_logs_detailed()
232
+
233
+ Returns:
234
+ Formatted string like browser DevTools output
235
+ """
236
+ if not logs:
237
+ return "No console logs captured."
238
+
239
+ level_icons = {
240
+ "error": "[ERROR]",
241
+ "warn": "[WARN]",
242
+ "info": "[INFO]",
243
+ "debug": "[DEBUG]",
244
+ "log": "[LOG]",
245
+ }
246
+
247
+ output = []
248
+ for entry in logs:
249
+ if isinstance(entry, dict):
250
+ level = entry.get("level", "log")
251
+ source = entry.get("source", "")
252
+ message = entry.get("message", "")
253
+ icon = level_icons.get(level, "[LOG]")
254
+ source_str = f" {source}" if source else ""
255
+ output.append(f"{icon}{source_str} {message}")
256
+ else:
257
+ output.append(str(entry))
258
+
259
+ return "\n".join(output)
260
+
261
+
262
+ def save_failure_artifacts(
263
+ page: "Page",
264
+ test_name: str,
265
+ artifacts_dir: Path | str,
266
+ console_logs: list | None = None,
267
+ ) -> dict:
268
+ """Save screenshot, console logs, and page HTML on test failure.
269
+
270
+ Args:
271
+ page: Playwright page object
272
+ test_name: Name of the failed test (e.g., request.node.nodeid)
273
+ artifacts_dir: Directory to save artifacts
274
+ console_logs: Pre-collected console logs (optional, will collect if None)
275
+
276
+ Returns:
277
+ Dict with paths to saved artifacts
278
+ """
279
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
280
+ safe_test_name = test_name.replace("::", "_").replace("[", "_").replace("]", "").replace("/", "_")
281
+
282
+ # Create artifacts directory with timestamp
283
+ artifacts_path = Path(artifacts_dir) / timestamp
284
+ artifacts_path.mkdir(parents=True, exist_ok=True)
285
+
286
+ saved_files = {}
287
+
288
+ # Collect console logs if not provided
289
+ if console_logs is None:
290
+ console_logs = collect_console_logs(page)
291
+
292
+ # Save screenshot
293
+ try:
294
+ screenshot_path = artifacts_path / f"{safe_test_name}_screenshot.png"
295
+ page.screenshot(path=str(screenshot_path), full_page=True)
296
+ saved_files["screenshot"] = screenshot_path
297
+ print(f"\n[FAILURE] Screenshot saved: {screenshot_path}")
298
+ except Exception as e:
299
+ print(f"\n[FAILURE] Failed to save screenshot: {e}")
300
+
301
+ # Save console logs
302
+ try:
303
+ logs_path = artifacts_path / f"{safe_test_name}_console.log"
304
+ with open(logs_path, "w") as f:
305
+ f.write(f"Test: {test_name}\n")
306
+ f.write(f"Timestamp: {timestamp}\n")
307
+ f.write(f"URL: {page.url}\n")
308
+ f.write("=" * 80 + "\n\n")
309
+ f.write("Console Logs:\n")
310
+ f.write("-" * 40 + "\n")
311
+ for log in console_logs:
312
+ f.write(f"{log}\n")
313
+ saved_files["console_logs"] = logs_path
314
+ print(f"[FAILURE] Console logs saved: {logs_path}")
315
+ except Exception as e:
316
+ print(f"[FAILURE] Failed to save console logs: {e}")
317
+
318
+ # Save page HTML
319
+ try:
320
+ html_path = artifacts_path / f"{safe_test_name}_page.html"
321
+ html_content = page.content()
322
+ with open(html_path, "w") as f:
323
+ f.write(html_content)
324
+ saved_files["page_html"] = html_path
325
+ print(f"[FAILURE] Page HTML saved: {html_path}")
326
+ except Exception as e:
327
+ print(f"[FAILURE] Failed to save page HTML: {e}")
328
+
329
+ return saved_files
330
+
331
+
332
+ def create_failure_capture_fixture(artifacts_dir: Path | str):
333
+ """Create a pytest fixture for automatic failure capture.
334
+
335
+ Usage in conftest.py:
336
+ from scitex.browser.debugging import create_failure_capture_fixture
337
+
338
+ capture_on_failure = create_failure_capture_fixture(
339
+ Path(__file__).parent / "artifacts"
340
+ )
341
+
342
+ Args:
343
+ artifacts_dir: Directory to save failure artifacts
344
+
345
+ Returns:
346
+ A pytest fixture function
347
+ """
348
+ import pytest
349
+
350
+ @pytest.fixture(autouse=True)
351
+ def capture_on_failure(request, page):
352
+ """Automatically capture console logs and screenshot on test failure."""
353
+ setup_console_interceptor(page)
354
+ yield
355
+ if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
356
+ console_logs = collect_console_logs(page)
357
+ save_failure_artifacts(page, request.node.nodeid, artifacts_dir, console_logs)
358
+
359
+ return capture_on_failure
360
+
361
+
362
+ # Pytest hook for capturing test results - add to conftest.py
363
+ PYTEST_HOOK_CODE = '''
364
+ @pytest.hookimpl(tryfirst=True, hookwrapper=True)
365
+ def pytest_runtest_makereport(item, call):
366
+ """Hook to capture test outcome for use in fixture."""
367
+ outcome = yield
368
+ rep = outcome.get_result()
369
+ setattr(item, f"rep_{rep.when}", rep)
370
+ '''
371
+
372
+ # EOF