scitex 2.4.1__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/auth/__init__.py +35 -0
- scitex/browser/auth/google.py +381 -0
- scitex/browser/collaboration/__init__.py +5 -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/README.md +117 -0
- scitex/scholar/citation_graph/__init__.py +29 -0
- scitex/scholar/citation_graph/builder.py +214 -0
- scitex/scholar/citation_graph/database.py +246 -0
- scitex/scholar/citation_graph/example.py +96 -0
- scitex/scholar/citation_graph/models.py +80 -0
- scitex/scholar/config/ScholarConfig.py +23 -3
- scitex/scholar/config/default.yaml +56 -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/metadata_engines/ScholarEngine.py +9 -1
- scitex/scholar/metadata_engines/individual/CrossRefLocalEngine.py +82 -21
- 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 +27 -9
- scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +24 -8
- scitex/scholar/search_engines/ScholarSearchEngine.py +6 -1
- {scitex-2.4.1.dist-info → scitex-2.4.3.dist-info}/METADATA +1 -1
- {scitex-2.4.1.dist-info → scitex-2.4.3.dist-info}/RECORD +36 -20
- {scitex-2.4.1.dist-info → scitex-2.4.3.dist-info}/WHEEL +0 -0
- {scitex-2.4.1.dist-info → scitex-2.4.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.1.dist-info → scitex-2.4.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
@@ -0,0 +1,432 @@
|
|
|
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/_visual_cursor.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Visual cursor and click effects for E2E test feedback.
|
|
8
|
+
|
|
9
|
+
Provides visual feedback during browser automation:
|
|
10
|
+
- Visual cursor indicator that follows mouse movements
|
|
11
|
+
- Click ripple effects
|
|
12
|
+
- Drag state visualization
|
|
13
|
+
- Step progress messages
|
|
14
|
+
|
|
15
|
+
Works with both async and sync Playwright APIs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Union
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from playwright.async_api import Page as AsyncPage
|
|
24
|
+
from playwright.sync_api import Page as SyncPage
|
|
25
|
+
|
|
26
|
+
# CSS styles for visual effects
|
|
27
|
+
VISUAL_EFFECTS_CSS = """
|
|
28
|
+
/* Visual cursor indicator */
|
|
29
|
+
#_scitex_cursor {
|
|
30
|
+
position: fixed;
|
|
31
|
+
width: 24px;
|
|
32
|
+
height: 24px;
|
|
33
|
+
border: 3px solid #FF4444;
|
|
34
|
+
border-radius: 50%;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
z-index: 2147483647;
|
|
37
|
+
transform: translate(-50%, -50%);
|
|
38
|
+
transition: all 0.15s ease-out;
|
|
39
|
+
box-shadow: 0 0 15px rgba(255, 68, 68, 0.6);
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
#_scitex_cursor.clicking {
|
|
43
|
+
transform: translate(-50%, -50%) scale(0.6);
|
|
44
|
+
background: rgba(255, 68, 68, 0.4);
|
|
45
|
+
box-shadow: 0 0 25px rgba(255, 68, 68, 0.8);
|
|
46
|
+
}
|
|
47
|
+
#_scitex_cursor.dragging {
|
|
48
|
+
border-color: #28A745;
|
|
49
|
+
box-shadow: 0 0 15px rgba(40, 167, 69, 0.6);
|
|
50
|
+
width: 28px;
|
|
51
|
+
height: 28px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Click ripple effect */
|
|
55
|
+
.scitex-click-ripple {
|
|
56
|
+
position: fixed;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
border: 3px solid #FF4444;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
z-index: 2147483646;
|
|
61
|
+
animation: clickRipple 0.5s ease-out forwards;
|
|
62
|
+
}
|
|
63
|
+
@keyframes clickRipple {
|
|
64
|
+
0% { width: 0; height: 0; opacity: 1; transform: translate(-50%, -50%); }
|
|
65
|
+
100% { width: 80px; height: 80px; opacity: 0; transform: translate(-50%, -50%); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Step message container */
|
|
69
|
+
#_scitex_step_messages {
|
|
70
|
+
position: fixed;
|
|
71
|
+
top: 10px;
|
|
72
|
+
left: 10px;
|
|
73
|
+
z-index: 2147483647;
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 8px;
|
|
77
|
+
max-width: 600px;
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
}
|
|
80
|
+
.scitex-step-msg {
|
|
81
|
+
background: rgba(0, 0, 0, 0.9);
|
|
82
|
+
color: white;
|
|
83
|
+
padding: 14px 24px;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
font-family: 'Courier New', monospace;
|
|
87
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
88
|
+
word-wrap: break-word;
|
|
89
|
+
animation: stepSlideIn 0.3s ease-out;
|
|
90
|
+
}
|
|
91
|
+
@keyframes stepSlideIn {
|
|
92
|
+
0% { opacity: 0; transform: translateX(-20px); }
|
|
93
|
+
100% { opacity: 1; transform: translateX(0); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Test result banner */
|
|
97
|
+
#_scitex_result_banner {
|
|
98
|
+
position: fixed;
|
|
99
|
+
top: 50%;
|
|
100
|
+
left: 50%;
|
|
101
|
+
transform: translate(-50%, -50%);
|
|
102
|
+
padding: 40px 80px;
|
|
103
|
+
border-radius: 16px;
|
|
104
|
+
font-size: 48px;
|
|
105
|
+
font-weight: bold;
|
|
106
|
+
font-family: 'Arial', sans-serif;
|
|
107
|
+
z-index: 2147483647;
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
animation: resultPulse 0.5s ease-out;
|
|
110
|
+
}
|
|
111
|
+
#_scitex_result_banner.success {
|
|
112
|
+
background: rgba(40, 167, 69, 0.95);
|
|
113
|
+
color: white;
|
|
114
|
+
box-shadow: 0 0 50px rgba(40, 167, 69, 0.8);
|
|
115
|
+
}
|
|
116
|
+
#_scitex_result_banner.failure {
|
|
117
|
+
background: rgba(220, 53, 69, 0.95);
|
|
118
|
+
color: white;
|
|
119
|
+
box-shadow: 0 0 50px rgba(220, 53, 69, 0.8);
|
|
120
|
+
}
|
|
121
|
+
@keyframes resultPulse {
|
|
122
|
+
0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
|
|
123
|
+
50% { transform: translate(-50%, -50%) scale(1.1); }
|
|
124
|
+
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
|
125
|
+
}
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# JavaScript to inject visual effects
|
|
129
|
+
INJECT_EFFECTS_JS = f"""
|
|
130
|
+
() => {{
|
|
131
|
+
if (document.getElementById('_scitex_visual_effects')) return;
|
|
132
|
+
|
|
133
|
+
const style = document.createElement('style');
|
|
134
|
+
style.id = '_scitex_visual_effects';
|
|
135
|
+
style.textContent = `{VISUAL_EFFECTS_CSS}`;
|
|
136
|
+
document.head.appendChild(style);
|
|
137
|
+
|
|
138
|
+
// Create cursor element
|
|
139
|
+
const cursor = document.createElement('div');
|
|
140
|
+
cursor.id = '_scitex_cursor';
|
|
141
|
+
document.body.appendChild(cursor);
|
|
142
|
+
|
|
143
|
+
// Create step messages container
|
|
144
|
+
const msgContainer = document.createElement('div');
|
|
145
|
+
msgContainer.id = '_scitex_step_messages';
|
|
146
|
+
document.body.appendChild(msgContainer);
|
|
147
|
+
}}
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def inject_visual_effects(page: Union["AsyncPage", "SyncPage"]) -> None:
|
|
152
|
+
"""Inject CSS and elements for visual effects (sync version)."""
|
|
153
|
+
page.evaluate(INJECT_EFFECTS_JS)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def inject_visual_effects_async(page: "AsyncPage") -> None:
|
|
157
|
+
"""Inject CSS and elements for visual effects (async version)."""
|
|
158
|
+
await page.evaluate(INJECT_EFFECTS_JS)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def show_cursor_at(
|
|
162
|
+
page: Union["AsyncPage", "SyncPage"],
|
|
163
|
+
x: float,
|
|
164
|
+
y: float,
|
|
165
|
+
state: str = "normal"
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Move visual cursor to position (sync version).
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
page: Playwright page object
|
|
171
|
+
x: X coordinate
|
|
172
|
+
y: Y coordinate
|
|
173
|
+
state: Cursor state - "normal", "clicking", or "dragging"
|
|
174
|
+
"""
|
|
175
|
+
page.evaluate("""
|
|
176
|
+
([x, y, state]) => {
|
|
177
|
+
let cursor = document.getElementById('_scitex_cursor');
|
|
178
|
+
if (!cursor) {
|
|
179
|
+
cursor = document.createElement('div');
|
|
180
|
+
cursor.id = '_scitex_cursor';
|
|
181
|
+
document.body.appendChild(cursor);
|
|
182
|
+
}
|
|
183
|
+
cursor.style.display = 'block';
|
|
184
|
+
cursor.style.left = x + 'px';
|
|
185
|
+
cursor.style.top = y + 'px';
|
|
186
|
+
cursor.className = state === 'clicking' ? 'clicking' :
|
|
187
|
+
state === 'dragging' ? 'dragging' : '';
|
|
188
|
+
if (state === 'dragging') {
|
|
189
|
+
cursor.style.borderColor = '#28A745';
|
|
190
|
+
cursor.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.6)';
|
|
191
|
+
} else {
|
|
192
|
+
cursor.style.borderColor = '#FF4444';
|
|
193
|
+
cursor.style.boxShadow = '0 0 15px rgba(255, 68, 68, 0.6)';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
""", [x, y, state])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def show_cursor_at_async(
|
|
200
|
+
page: "AsyncPage",
|
|
201
|
+
x: float,
|
|
202
|
+
y: float,
|
|
203
|
+
state: str = "normal"
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Move visual cursor to position (async version)."""
|
|
206
|
+
await page.evaluate("""
|
|
207
|
+
([x, y, state]) => {
|
|
208
|
+
let cursor = document.getElementById('_scitex_cursor');
|
|
209
|
+
if (!cursor) {
|
|
210
|
+
cursor = document.createElement('div');
|
|
211
|
+
cursor.id = '_scitex_cursor';
|
|
212
|
+
document.body.appendChild(cursor);
|
|
213
|
+
}
|
|
214
|
+
cursor.style.display = 'block';
|
|
215
|
+
cursor.style.left = x + 'px';
|
|
216
|
+
cursor.style.top = y + 'px';
|
|
217
|
+
cursor.className = state === 'clicking' ? 'clicking' :
|
|
218
|
+
state === 'dragging' ? 'dragging' : '';
|
|
219
|
+
if (state === 'dragging') {
|
|
220
|
+
cursor.style.borderColor = '#28A745';
|
|
221
|
+
cursor.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.6)';
|
|
222
|
+
} else {
|
|
223
|
+
cursor.style.borderColor = '#FF4444';
|
|
224
|
+
cursor.style.boxShadow = '0 0 15px rgba(255, 68, 68, 0.6)';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
""", [x, y, state])
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def show_click_effect(page: Union["AsyncPage", "SyncPage"], x: float, y: float) -> None:
|
|
231
|
+
"""Show click ripple effect at position (sync version)."""
|
|
232
|
+
page.evaluate("""
|
|
233
|
+
([x, y]) => {
|
|
234
|
+
const ripple = document.createElement('div');
|
|
235
|
+
ripple.className = 'scitex-click-ripple';
|
|
236
|
+
ripple.style.left = x + 'px';
|
|
237
|
+
ripple.style.top = y + 'px';
|
|
238
|
+
document.body.appendChild(ripple);
|
|
239
|
+
setTimeout(() => ripple.remove(), 600);
|
|
240
|
+
|
|
241
|
+
const cursor = document.getElementById('_scitex_cursor');
|
|
242
|
+
if (cursor) {
|
|
243
|
+
cursor.classList.add('clicking');
|
|
244
|
+
setTimeout(() => cursor.classList.remove('clicking'), 150);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
""", [x, y])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def show_click_effect_async(page: "AsyncPage", x: float, y: float) -> None:
|
|
251
|
+
"""Show click ripple effect at position (async version)."""
|
|
252
|
+
await page.evaluate("""
|
|
253
|
+
([x, y]) => {
|
|
254
|
+
const ripple = document.createElement('div');
|
|
255
|
+
ripple.className = 'scitex-click-ripple';
|
|
256
|
+
ripple.style.left = x + 'px';
|
|
257
|
+
ripple.style.top = y + 'px';
|
|
258
|
+
document.body.appendChild(ripple);
|
|
259
|
+
setTimeout(() => ripple.remove(), 600);
|
|
260
|
+
|
|
261
|
+
const cursor = document.getElementById('_scitex_cursor');
|
|
262
|
+
if (cursor) {
|
|
263
|
+
cursor.classList.add('clicking');
|
|
264
|
+
setTimeout(() => cursor.classList.remove('clicking'), 150);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
""", [x, y])
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def show_step(
|
|
271
|
+
page: Union["AsyncPage", "SyncPage"],
|
|
272
|
+
step: int,
|
|
273
|
+
total: int,
|
|
274
|
+
message: str,
|
|
275
|
+
level: str = "info"
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Show numbered step message in browser (sync version).
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
page: Playwright page object
|
|
281
|
+
step: Current step number
|
|
282
|
+
total: Total number of steps
|
|
283
|
+
message: Message to display
|
|
284
|
+
level: Message level - "info", "success", "warning", or "error"
|
|
285
|
+
"""
|
|
286
|
+
color_map = {
|
|
287
|
+
"info": "#17A2B8",
|
|
288
|
+
"success": "#28A745",
|
|
289
|
+
"warning": "#FFC107",
|
|
290
|
+
"error": "#DC3545",
|
|
291
|
+
}
|
|
292
|
+
color = color_map.get(level, color_map["info"])
|
|
293
|
+
|
|
294
|
+
page.evaluate("""
|
|
295
|
+
([step, total, message, color]) => {
|
|
296
|
+
let container = document.getElementById('_scitex_step_messages');
|
|
297
|
+
if (!container) {
|
|
298
|
+
container = document.createElement('div');
|
|
299
|
+
container.id = '_scitex_step_messages';
|
|
300
|
+
container.style.cssText = `
|
|
301
|
+
position: fixed; top: 10px; left: 10px; z-index: 2147483647;
|
|
302
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
303
|
+
max-width: 600px; pointer-events: none;
|
|
304
|
+
`;
|
|
305
|
+
document.body.appendChild(container);
|
|
306
|
+
}
|
|
307
|
+
const popup = document.createElement('div');
|
|
308
|
+
popup.className = 'scitex-step-msg';
|
|
309
|
+
popup.innerHTML = `<strong>[${step}/${total}] ${message}</strong>`;
|
|
310
|
+
popup.style.cssText = `
|
|
311
|
+
background: rgba(0, 0, 0, 0.9); color: white;
|
|
312
|
+
padding: 14px 24px; border-radius: 8px; font-size: 16px;
|
|
313
|
+
font-family: 'Courier New', monospace;
|
|
314
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
315
|
+
border-left: 6px solid ${color}; word-wrap: break-word;
|
|
316
|
+
`;
|
|
317
|
+
container.appendChild(popup);
|
|
318
|
+
while (container.children.length > 5) container.removeChild(container.firstChild);
|
|
319
|
+
setTimeout(() => { if (popup.parentNode) popup.parentNode.removeChild(popup); }, 8000);
|
|
320
|
+
}
|
|
321
|
+
""", [step, total, message, color])
|
|
322
|
+
page.wait_for_timeout(200)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
async def show_step_async(
|
|
326
|
+
page: "AsyncPage",
|
|
327
|
+
step: int,
|
|
328
|
+
total: int,
|
|
329
|
+
message: str,
|
|
330
|
+
level: str = "info"
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Show numbered step message in browser (async version)."""
|
|
333
|
+
color_map = {
|
|
334
|
+
"info": "#17A2B8",
|
|
335
|
+
"success": "#28A745",
|
|
336
|
+
"warning": "#FFC107",
|
|
337
|
+
"error": "#DC3545",
|
|
338
|
+
}
|
|
339
|
+
color = color_map.get(level, color_map["info"])
|
|
340
|
+
|
|
341
|
+
await page.evaluate("""
|
|
342
|
+
([step, total, message, color]) => {
|
|
343
|
+
let container = document.getElementById('_scitex_step_messages');
|
|
344
|
+
if (!container) {
|
|
345
|
+
container = document.createElement('div');
|
|
346
|
+
container.id = '_scitex_step_messages';
|
|
347
|
+
container.style.cssText = `
|
|
348
|
+
position: fixed; top: 10px; left: 10px; z-index: 2147483647;
|
|
349
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
350
|
+
max-width: 600px; pointer-events: none;
|
|
351
|
+
`;
|
|
352
|
+
document.body.appendChild(container);
|
|
353
|
+
}
|
|
354
|
+
const popup = document.createElement('div');
|
|
355
|
+
popup.className = 'scitex-step-msg';
|
|
356
|
+
popup.innerHTML = `<strong>[${step}/${total}] ${message}</strong>`;
|
|
357
|
+
popup.style.cssText = `
|
|
358
|
+
background: rgba(0, 0, 0, 0.9); color: white;
|
|
359
|
+
padding: 14px 24px; border-radius: 8px; font-size: 16px;
|
|
360
|
+
font-family: 'Courier New', monospace;
|
|
361
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
362
|
+
border-left: 6px solid ${color}; word-wrap: break-word;
|
|
363
|
+
`;
|
|
364
|
+
container.appendChild(popup);
|
|
365
|
+
while (container.children.length > 5) container.removeChild(container.firstChild);
|
|
366
|
+
setTimeout(() => { if (popup.parentNode) popup.parentNode.removeChild(popup); }, 8000);
|
|
367
|
+
}
|
|
368
|
+
""", [step, total, message, color])
|
|
369
|
+
await page.wait_for_timeout(200)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def show_test_result(
|
|
373
|
+
page: Union["AsyncPage", "SyncPage"],
|
|
374
|
+
success: bool,
|
|
375
|
+
message: str = "",
|
|
376
|
+
delay_ms: int = 3000
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Show test result banner (PASS/FAIL) and wait (sync version).
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
page: Playwright page object
|
|
382
|
+
success: True for PASS, False for FAIL
|
|
383
|
+
message: Optional message to display
|
|
384
|
+
delay_ms: How long to display before continuing
|
|
385
|
+
"""
|
|
386
|
+
status = "PASS" if success else "FAIL"
|
|
387
|
+
css_class = "success" if success else "failure"
|
|
388
|
+
display_text = f"{status}" + (f": {message}" if message else "")
|
|
389
|
+
|
|
390
|
+
page.evaluate("""
|
|
391
|
+
([displayText, cssClass]) => {
|
|
392
|
+
// Remove existing banner
|
|
393
|
+
const existing = document.getElementById('_scitex_result_banner');
|
|
394
|
+
if (existing) existing.remove();
|
|
395
|
+
|
|
396
|
+
const banner = document.createElement('div');
|
|
397
|
+
banner.id = '_scitex_result_banner';
|
|
398
|
+
banner.className = cssClass;
|
|
399
|
+
banner.textContent = displayText;
|
|
400
|
+
document.body.appendChild(banner);
|
|
401
|
+
}
|
|
402
|
+
""", [display_text, css_class])
|
|
403
|
+
page.wait_for_timeout(delay_ms)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def show_test_result_async(
|
|
407
|
+
page: "AsyncPage",
|
|
408
|
+
success: bool,
|
|
409
|
+
message: str = "",
|
|
410
|
+
delay_ms: int = 3000
|
|
411
|
+
) -> None:
|
|
412
|
+
"""Show test result banner (PASS/FAIL) and wait (async version)."""
|
|
413
|
+
status = "PASS" if success else "FAIL"
|
|
414
|
+
css_class = "success" if success else "failure"
|
|
415
|
+
display_text = f"{status}" + (f": {message}" if message else "")
|
|
416
|
+
|
|
417
|
+
await page.evaluate("""
|
|
418
|
+
([displayText, cssClass]) => {
|
|
419
|
+
const existing = document.getElementById('_scitex_result_banner');
|
|
420
|
+
if (existing) existing.remove();
|
|
421
|
+
|
|
422
|
+
const banner = document.createElement('div');
|
|
423
|
+
banner.id = '_scitex_result_banner';
|
|
424
|
+
banner.className = cssClass;
|
|
425
|
+
banner.textContent = displayText;
|
|
426
|
+
document.body.appendChild(banner);
|
|
427
|
+
}
|
|
428
|
+
""", [display_text, css_class])
|
|
429
|
+
await page.wait_for_timeout(delay_ms)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# EOF
|