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
|
@@ -0,0 +1,259 @@
|
|
|
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/_sync_session.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Sync browser session context manager for pytest-playwright E2E tests.
|
|
8
|
+
|
|
9
|
+
Ensures proper cleanup of browser processes to prevent zombies.
|
|
10
|
+
|
|
11
|
+
Usage in conftest.py:
|
|
12
|
+
from scitex.browser import SyncBrowserSession
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def browser_session(page: Page):
|
|
16
|
+
with SyncBrowserSession(page) as session:
|
|
17
|
+
yield session
|
|
18
|
+
# Cleanup happens automatically even on exceptions
|
|
19
|
+
|
|
20
|
+
Or use the fixture factory:
|
|
21
|
+
from scitex.browser import create_browser_session_fixture
|
|
22
|
+
browser_session = create_browser_session_fixture()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import atexit
|
|
26
|
+
import os
|
|
27
|
+
import signal
|
|
28
|
+
import subprocess
|
|
29
|
+
from contextlib import contextmanager
|
|
30
|
+
from typing import TYPE_CHECKING, Callable, Optional
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from playwright.sync_api import Page
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SyncBrowserSession:
|
|
37
|
+
"""
|
|
38
|
+
Sync context manager for playwright browser sessions.
|
|
39
|
+
|
|
40
|
+
Ensures zombie process cleanup on test failures, timeouts, or crashes.
|
|
41
|
+
Tracks browser PIDs and kills orphaned processes on exit.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Class-level tracking of active sessions for emergency cleanup
|
|
45
|
+
_active_sessions: list["SyncBrowserSession"] = []
|
|
46
|
+
_cleanup_registered = False
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
page: "Page",
|
|
51
|
+
timeout: int = 60,
|
|
52
|
+
on_enter: Optional[Callable[["Page"], None]] = None,
|
|
53
|
+
on_exit: Optional[Callable[["Page", bool], None]] = None,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize sync browser session.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
page: Playwright page instance from pytest-playwright
|
|
60
|
+
timeout: Default timeout for operations in seconds
|
|
61
|
+
on_enter: Callback when entering context
|
|
62
|
+
on_exit: Callback when exiting context (receives page and success flag)
|
|
63
|
+
"""
|
|
64
|
+
self.page = page
|
|
65
|
+
self.timeout = timeout
|
|
66
|
+
self.on_enter = on_enter
|
|
67
|
+
self.on_exit = on_exit
|
|
68
|
+
self._browser_pid = None
|
|
69
|
+
self._context_pid = None
|
|
70
|
+
self._success = True
|
|
71
|
+
|
|
72
|
+
# Register class-level emergency cleanup
|
|
73
|
+
if not SyncBrowserSession._cleanup_registered:
|
|
74
|
+
atexit.register(SyncBrowserSession._emergency_cleanup)
|
|
75
|
+
SyncBrowserSession._cleanup_registered = True
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> "SyncBrowserSession":
|
|
78
|
+
"""Enter context - track browser PIDs and run setup callback."""
|
|
79
|
+
# Track this session
|
|
80
|
+
SyncBrowserSession._active_sessions.append(self)
|
|
81
|
+
|
|
82
|
+
# Try to get browser PID for tracking
|
|
83
|
+
try:
|
|
84
|
+
if self.page.context.browser:
|
|
85
|
+
# Get the browser process
|
|
86
|
+
browser = self.page.context.browser
|
|
87
|
+
# Browser PID is available via internal _impl
|
|
88
|
+
if hasattr(browser, '_impl'):
|
|
89
|
+
impl = browser._impl
|
|
90
|
+
if hasattr(impl, '_process'):
|
|
91
|
+
self._browser_pid = impl._process.pid
|
|
92
|
+
except Exception:
|
|
93
|
+
pass # PID tracking is best-effort
|
|
94
|
+
|
|
95
|
+
# Run setup callback
|
|
96
|
+
if self.on_enter:
|
|
97
|
+
self.on_enter(self.page)
|
|
98
|
+
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
102
|
+
"""Exit context - ensure cleanup happens."""
|
|
103
|
+
self._success = exc_type is None
|
|
104
|
+
|
|
105
|
+
# Remove from active sessions
|
|
106
|
+
try:
|
|
107
|
+
SyncBrowserSession._active_sessions.remove(self)
|
|
108
|
+
except ValueError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# Run exit callback
|
|
112
|
+
if self.on_exit:
|
|
113
|
+
try:
|
|
114
|
+
self.on_exit(self.page, self._success)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass # Don't fail on callback errors
|
|
117
|
+
|
|
118
|
+
# If there was an exception, try to close gracefully
|
|
119
|
+
if exc_type is not None:
|
|
120
|
+
try:
|
|
121
|
+
self.page.close()
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
self.page.context.close()
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
# Kill orphaned browser process if we have the PID
|
|
131
|
+
if self._browser_pid and not self._success:
|
|
132
|
+
self._kill_process_tree(self._browser_pid)
|
|
133
|
+
|
|
134
|
+
# Don't suppress the exception
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _kill_process_tree(pid: int):
|
|
139
|
+
"""Kill a process and all its children (zombies)."""
|
|
140
|
+
try:
|
|
141
|
+
# Try SIGTERM first
|
|
142
|
+
os.kill(pid, signal.SIGTERM)
|
|
143
|
+
except ProcessLookupError:
|
|
144
|
+
return # Already dead
|
|
145
|
+
except PermissionError:
|
|
146
|
+
return # Can't kill
|
|
147
|
+
|
|
148
|
+
# Give it a moment
|
|
149
|
+
import time
|
|
150
|
+
time.sleep(0.5)
|
|
151
|
+
|
|
152
|
+
# Force kill if still running
|
|
153
|
+
try:
|
|
154
|
+
os.kill(pid, signal.SIGKILL)
|
|
155
|
+
except (ProcessLookupError, PermissionError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def _emergency_cleanup(cls):
|
|
160
|
+
"""Emergency cleanup of all active sessions on process exit."""
|
|
161
|
+
for session in cls._active_sessions[:]: # Copy list to avoid mutation
|
|
162
|
+
if session._browser_pid:
|
|
163
|
+
cls._kill_process_tree(session._browser_pid)
|
|
164
|
+
cls._active_sessions.clear()
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def kill_zombie_browsers():
|
|
168
|
+
"""Kill all zombie chromium/chrome processes from failed tests.
|
|
169
|
+
|
|
170
|
+
Call this at the start of test sessions to clean up from previous runs.
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
# Find orphaned chromium processes
|
|
174
|
+
result = subprocess.run(
|
|
175
|
+
["pgrep", "-f", "chromium|chrome"],
|
|
176
|
+
capture_output=True,
|
|
177
|
+
text=True,
|
|
178
|
+
)
|
|
179
|
+
if result.returncode == 0:
|
|
180
|
+
pids = result.stdout.strip().split('\n')
|
|
181
|
+
for pid in pids:
|
|
182
|
+
if pid:
|
|
183
|
+
try:
|
|
184
|
+
os.kill(int(pid), signal.SIGKILL)
|
|
185
|
+
except (ProcessLookupError, PermissionError, ValueError):
|
|
186
|
+
pass
|
|
187
|
+
except FileNotFoundError:
|
|
188
|
+
pass # pgrep not available
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@contextmanager
|
|
192
|
+
def sync_browser_session(
|
|
193
|
+
page: "Page",
|
|
194
|
+
timeout: int = 60,
|
|
195
|
+
on_enter: Optional[Callable[["Page"], None]] = None,
|
|
196
|
+
on_exit: Optional[Callable[["Page", bool], None]] = None,
|
|
197
|
+
):
|
|
198
|
+
"""
|
|
199
|
+
Context manager for sync playwright sessions.
|
|
200
|
+
|
|
201
|
+
Usage:
|
|
202
|
+
with sync_browser_session(page) as session:
|
|
203
|
+
session.page.goto(url)
|
|
204
|
+
# ... test code
|
|
205
|
+
# Cleanup happens automatically
|
|
206
|
+
"""
|
|
207
|
+
session = SyncBrowserSession(page, timeout, on_enter, on_exit)
|
|
208
|
+
with session:
|
|
209
|
+
yield session
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def create_browser_session_fixture(
|
|
213
|
+
timeout: int = 60,
|
|
214
|
+
setup: Optional[Callable[["Page"], None]] = None,
|
|
215
|
+
teardown: Optional[Callable[["Page", bool], None]] = None,
|
|
216
|
+
kill_zombies_on_start: bool = True,
|
|
217
|
+
):
|
|
218
|
+
"""
|
|
219
|
+
Create a pytest fixture for browser session with cleanup.
|
|
220
|
+
|
|
221
|
+
Usage in conftest.py:
|
|
222
|
+
from scitex.browser import create_browser_session_fixture
|
|
223
|
+
|
|
224
|
+
browser_session = create_browser_session_fixture(
|
|
225
|
+
timeout=60,
|
|
226
|
+
setup=lambda page: print(f"Starting test"),
|
|
227
|
+
teardown=lambda page, success: print(f"Test {'passed' if success else 'failed'}"),
|
|
228
|
+
kill_zombies_on_start=True,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
timeout: Default timeout for operations
|
|
233
|
+
setup: Callback when entering session
|
|
234
|
+
teardown: Callback when exiting (receives page and success flag)
|
|
235
|
+
kill_zombies_on_start: Kill orphaned browsers before first test
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
A pytest fixture function
|
|
239
|
+
"""
|
|
240
|
+
import pytest
|
|
241
|
+
|
|
242
|
+
_zombies_cleaned = False
|
|
243
|
+
|
|
244
|
+
@pytest.fixture
|
|
245
|
+
def browser_session(page: "Page"):
|
|
246
|
+
nonlocal _zombies_cleaned
|
|
247
|
+
|
|
248
|
+
# Clean up zombies from previous runs (once per session)
|
|
249
|
+
if kill_zombies_on_start and not _zombies_cleaned:
|
|
250
|
+
SyncBrowserSession.kill_zombie_browsers()
|
|
251
|
+
_zombies_cleaned = True
|
|
252
|
+
|
|
253
|
+
with SyncBrowserSession(page, timeout, setup, teardown) as session:
|
|
254
|
+
yield session
|
|
255
|
+
|
|
256
|
+
return browser_session
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# EOF
|
|
@@ -0,0 +1,284 @@
|
|
|
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/_test_monitor.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Test monitoring with periodic screenshots using scitex.capture.
|
|
8
|
+
|
|
9
|
+
Provides automated visual monitoring during E2E tests:
|
|
10
|
+
- Periodic screenshots at configurable intervals
|
|
11
|
+
- Integration with scitex.capture for WSL/Windows support
|
|
12
|
+
- GIF generation from test sessions
|
|
13
|
+
- Pytest fixture integration
|
|
14
|
+
|
|
15
|
+
Usage in conftest.py:
|
|
16
|
+
from scitex.browser.debugging import (
|
|
17
|
+
create_test_monitor,
|
|
18
|
+
TestMonitor,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def test_monitor():
|
|
23
|
+
monitor = TestMonitor(interval=2.0, verbose=True)
|
|
24
|
+
monitor.start()
|
|
25
|
+
yield monitor
|
|
26
|
+
monitor.stop()
|
|
27
|
+
monitor.create_gif() # Optional: create GIF from screenshots
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import TYPE_CHECKING, Optional
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from playwright.sync_api import Page
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestMonitor:
|
|
39
|
+
"""
|
|
40
|
+
Monitor E2E tests with periodic screenshots using scitex.capture.
|
|
41
|
+
|
|
42
|
+
Captures screenshots at regular intervals during test execution,
|
|
43
|
+
allowing visual inspection of test progress and debugging.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
output_dir: str | Path = None,
|
|
49
|
+
interval: float = 2.0,
|
|
50
|
+
quality: int = 70,
|
|
51
|
+
verbose: bool = False,
|
|
52
|
+
test_name: str = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize test monitor.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
output_dir: Directory for screenshots (default: ~/.scitex/test_monitor)
|
|
59
|
+
interval: Seconds between screenshots (default: 2.0)
|
|
60
|
+
quality: JPEG quality 1-100 (default: 70)
|
|
61
|
+
verbose: Print capture messages
|
|
62
|
+
test_name: Optional test name for session identification
|
|
63
|
+
"""
|
|
64
|
+
self.output_dir = Path(output_dir or Path.home() / ".scitex" / "test_monitor")
|
|
65
|
+
self.interval = interval
|
|
66
|
+
self.quality = quality
|
|
67
|
+
self.verbose = verbose
|
|
68
|
+
self.test_name = test_name
|
|
69
|
+
self.session_id = None
|
|
70
|
+
self._worker = None
|
|
71
|
+
self._capture_manager = None
|
|
72
|
+
|
|
73
|
+
def start(self, test_name: str = None) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Start periodic screenshot capture.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
test_name: Optional test name to include in session
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Session ID for this capture session
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
from scitex.capture import CaptureManager
|
|
85
|
+
except ImportError:
|
|
86
|
+
if self.verbose:
|
|
87
|
+
print("[TestMonitor] scitex.capture not available")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# Generate session ID
|
|
91
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
92
|
+
name = test_name or self.test_name or "test"
|
|
93
|
+
safe_name = name.replace("::", "_").replace("[", "_").replace("]", "").replace("/", "_")
|
|
94
|
+
self.session_id = f"{timestamp}_{safe_name}"
|
|
95
|
+
|
|
96
|
+
# Create session directory
|
|
97
|
+
session_dir = self.output_dir / self.session_id
|
|
98
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
# Start capture
|
|
101
|
+
self._capture_manager = CaptureManager()
|
|
102
|
+
self._worker = self._capture_manager.start_capture(
|
|
103
|
+
output_dir=str(session_dir),
|
|
104
|
+
interval=self.interval,
|
|
105
|
+
jpeg=True,
|
|
106
|
+
quality=self.quality,
|
|
107
|
+
verbose=self.verbose,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if self.verbose:
|
|
111
|
+
print(f"[TestMonitor] Started: {session_dir} (interval: {self.interval}s)")
|
|
112
|
+
|
|
113
|
+
return self.session_id
|
|
114
|
+
|
|
115
|
+
def stop(self) -> dict:
|
|
116
|
+
"""
|
|
117
|
+
Stop screenshot capture.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Status dict with session info
|
|
121
|
+
"""
|
|
122
|
+
if self._capture_manager:
|
|
123
|
+
self._capture_manager.stop_capture()
|
|
124
|
+
|
|
125
|
+
status = self.get_status()
|
|
126
|
+
|
|
127
|
+
if self.verbose and self._worker:
|
|
128
|
+
print(f"[TestMonitor] Stopped: {self._worker.screenshot_count} screenshots")
|
|
129
|
+
|
|
130
|
+
return status
|
|
131
|
+
|
|
132
|
+
def get_status(self) -> dict:
|
|
133
|
+
"""Get current monitor status."""
|
|
134
|
+
if self._worker:
|
|
135
|
+
return self._worker.get_status()
|
|
136
|
+
return {
|
|
137
|
+
"running": False,
|
|
138
|
+
"session_id": self.session_id,
|
|
139
|
+
"output_dir": str(self.output_dir),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def take_snapshot(self, message: str = None) -> Optional[str]:
|
|
143
|
+
"""
|
|
144
|
+
Take an immediate snapshot (in addition to periodic captures).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
message: Optional message to include in filename
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Path to saved screenshot
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
from scitex.capture import snap
|
|
154
|
+
return snap(message=message, output_dir=str(self.output_dir))
|
|
155
|
+
except ImportError:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def create_gif(self, duration: float = 0.5, output_path: str = None) -> Optional[str]:
|
|
159
|
+
"""
|
|
160
|
+
Create GIF from captured screenshots.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
duration: Duration per frame in seconds
|
|
164
|
+
output_path: Output path for GIF (auto-generated if None)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Path to created GIF
|
|
168
|
+
"""
|
|
169
|
+
if not self.session_id:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
from scitex.capture import create_gif_from_session
|
|
174
|
+
|
|
175
|
+
session_dir = self.output_dir / self.session_id
|
|
176
|
+
if output_path is None:
|
|
177
|
+
output_path = str(session_dir / f"{self.session_id}.gif")
|
|
178
|
+
|
|
179
|
+
return create_gif_from_session(
|
|
180
|
+
session_id=self.session_id,
|
|
181
|
+
output_path=output_path,
|
|
182
|
+
duration=duration,
|
|
183
|
+
)
|
|
184
|
+
except ImportError:
|
|
185
|
+
if self.verbose:
|
|
186
|
+
print("[TestMonitor] GIF creation requires scitex.capture")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def get_screenshots(self) -> list:
|
|
190
|
+
"""Get list of captured screenshot paths."""
|
|
191
|
+
if not self.session_id:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
session_dir = self.output_dir / self.session_id
|
|
195
|
+
if not session_dir.exists():
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
return sorted(session_dir.glob("*.jpg")) + sorted(session_dir.glob("*.png"))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def create_test_monitor_fixture(
|
|
202
|
+
output_dir: str | Path = None,
|
|
203
|
+
interval: float = 2.0,
|
|
204
|
+
auto_gif: bool = False,
|
|
205
|
+
):
|
|
206
|
+
"""
|
|
207
|
+
Create a pytest fixture for test monitoring.
|
|
208
|
+
|
|
209
|
+
Usage in conftest.py:
|
|
210
|
+
from scitex.browser.debugging import create_test_monitor_fixture
|
|
211
|
+
|
|
212
|
+
test_monitor = create_test_monitor_fixture(interval=2.0, auto_gif=True)
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
output_dir: Directory for screenshots
|
|
216
|
+
interval: Seconds between screenshots
|
|
217
|
+
auto_gif: Create GIF automatically on test completion
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
A pytest fixture function
|
|
221
|
+
"""
|
|
222
|
+
import pytest
|
|
223
|
+
|
|
224
|
+
@pytest.fixture
|
|
225
|
+
def test_monitor(request):
|
|
226
|
+
"""Pytest fixture for visual test monitoring."""
|
|
227
|
+
monitor = TestMonitor(
|
|
228
|
+
output_dir=output_dir,
|
|
229
|
+
interval=interval,
|
|
230
|
+
verbose=True,
|
|
231
|
+
test_name=request.node.nodeid,
|
|
232
|
+
)
|
|
233
|
+
monitor.start()
|
|
234
|
+
yield monitor
|
|
235
|
+
monitor.stop()
|
|
236
|
+
if auto_gif:
|
|
237
|
+
gif_path = monitor.create_gif()
|
|
238
|
+
if gif_path:
|
|
239
|
+
print(f"[TestMonitor] GIF created: {gif_path}")
|
|
240
|
+
|
|
241
|
+
return test_monitor
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# Convenience function for quick monitoring
|
|
245
|
+
def monitor_test(
|
|
246
|
+
test_func=None,
|
|
247
|
+
interval: float = 2.0,
|
|
248
|
+
auto_gif: bool = False,
|
|
249
|
+
):
|
|
250
|
+
"""
|
|
251
|
+
Decorator for monitoring tests with periodic screenshots.
|
|
252
|
+
|
|
253
|
+
Usage:
|
|
254
|
+
@monitor_test(interval=1.0, auto_gif=True)
|
|
255
|
+
def test_my_feature(page):
|
|
256
|
+
# test code...
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
test_func: Test function (for use without parentheses)
|
|
260
|
+
interval: Seconds between screenshots
|
|
261
|
+
auto_gif: Create GIF on completion
|
|
262
|
+
"""
|
|
263
|
+
def decorator(func):
|
|
264
|
+
def wrapper(*args, **kwargs):
|
|
265
|
+
monitor = TestMonitor(
|
|
266
|
+
interval=interval,
|
|
267
|
+
verbose=True,
|
|
268
|
+
test_name=func.__name__,
|
|
269
|
+
)
|
|
270
|
+
monitor.start()
|
|
271
|
+
try:
|
|
272
|
+
return func(*args, **kwargs)
|
|
273
|
+
finally:
|
|
274
|
+
monitor.stop()
|
|
275
|
+
if auto_gif:
|
|
276
|
+
monitor.create_gif()
|
|
277
|
+
return wrapper
|
|
278
|
+
|
|
279
|
+
if test_func is not None:
|
|
280
|
+
return decorator(test_func)
|
|
281
|
+
return decorator
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# EOF
|