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 +1 -1
- scitex/browser/__init__.py +53 -0
- scitex/browser/debugging/__init__.py +56 -0
- scitex/browser/debugging/_failure_capture.py +372 -0
- scitex/browser/debugging/_sync_session.py +259 -0
- scitex/browser/debugging/_test_monitor.py +284 -0
- scitex/browser/debugging/_visual_cursor.py +432 -0
- scitex/scholar/citation_graph/database.py +9 -2
- scitex/scholar/config/ScholarConfig.py +23 -3
- scitex/scholar/config/default.yaml +55 -0
- scitex/scholar/core/Paper.py +102 -0
- scitex/scholar/core/__init__.py +44 -0
- scitex/scholar/core/journal_normalizer.py +524 -0
- scitex/scholar/core/oa_cache.py +285 -0
- scitex/scholar/core/open_access.py +457 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +137 -0
- scitex/scholar/pdf_download/strategies/__init__.py +6 -0
- scitex/scholar/pdf_download/strategies/open_access_download.py +186 -0
- scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +18 -3
- scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +15 -2
- {scitex-2.4.2.dist-info → scitex-2.4.3.dist-info}/METADATA +1 -1
- {scitex-2.4.2.dist-info → scitex-2.4.3.dist-info}/RECORD +25 -17
- {scitex-2.4.2.dist-info → scitex-2.4.3.dist-info}/WHEEL +0 -0
- {scitex-2.4.2.dist-info → scitex-2.4.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.2.dist-info → scitex-2.4.3.dist-info}/licenses/LICENSE +0 -0
scitex/__version__.py
CHANGED
scitex/browser/__init__.py
CHANGED
|
@@ -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
|