cursorflow 1.2.0__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.
- cursorflow/__init__.py +78 -0
- cursorflow/auto_updater.py +244 -0
- cursorflow/cli.py +408 -0
- cursorflow/core/agent.py +272 -0
- cursorflow/core/auth_handler.py +433 -0
- cursorflow/core/browser_controller.py +534 -0
- cursorflow/core/browser_engine.py +386 -0
- cursorflow/core/css_iterator.py +397 -0
- cursorflow/core/cursor_integration.py +744 -0
- cursorflow/core/cursorflow.py +649 -0
- cursorflow/core/error_correlator.py +322 -0
- cursorflow/core/event_correlator.py +182 -0
- cursorflow/core/file_change_monitor.py +548 -0
- cursorflow/core/log_collector.py +410 -0
- cursorflow/core/log_monitor.py +179 -0
- cursorflow/core/persistent_session.py +910 -0
- cursorflow/core/report_generator.py +282 -0
- cursorflow/log_sources/local_file.py +198 -0
- cursorflow/log_sources/ssh_remote.py +210 -0
- cursorflow/updater.py +512 -0
- cursorflow-1.2.0.dist-info/METADATA +444 -0
- cursorflow-1.2.0.dist-info/RECORD +25 -0
- cursorflow-1.2.0.dist-info/WHEEL +5 -0
- cursorflow-1.2.0.dist-info/entry_points.txt +2 -0
- cursorflow-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,649 @@
|
|
1
|
+
"""
|
2
|
+
CursorFlow - Main API Class
|
3
|
+
|
4
|
+
Simple, fast data collection engine that enables Cursor to autonomously test UI
|
5
|
+
and iterate on designs with immediate visual feedback.
|
6
|
+
|
7
|
+
Design Philosophy: Declarative Actions | Batch Execution | Universal Correlation
|
8
|
+
"""
|
9
|
+
|
10
|
+
import asyncio
|
11
|
+
import time
|
12
|
+
import logging
|
13
|
+
from typing import Dict, List, Optional, Any
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
from .browser_controller import BrowserController
|
17
|
+
from .log_collector import LogCollector
|
18
|
+
from .event_correlator import EventCorrelator
|
19
|
+
from .auth_handler import AuthHandler
|
20
|
+
from .css_iterator import CSSIterator
|
21
|
+
from .cursor_integration import CursorIntegration
|
22
|
+
from .persistent_session import PersistentSession, get_session_manager
|
23
|
+
|
24
|
+
|
25
|
+
class CursorFlow:
|
26
|
+
"""
|
27
|
+
Main CursorFlow interface - Simple data collection for Cursor analysis
|
28
|
+
|
29
|
+
Usage:
|
30
|
+
flow = CursorFlow("http://localhost:3000", {"source": "local", "paths": ["logs/app.log"]})
|
31
|
+
|
32
|
+
# Test UI flow
|
33
|
+
results = await flow.execute_and_collect([
|
34
|
+
{"navigate": "/dashboard"},
|
35
|
+
{"click": "#refresh"},
|
36
|
+
{"screenshot": "refreshed"}
|
37
|
+
])
|
38
|
+
|
39
|
+
# CSS iteration
|
40
|
+
visual_results = await flow.css_iteration_session(
|
41
|
+
base_actions=[{"navigate": "/page"}],
|
42
|
+
css_changes=[{"name": "fix", "css": ".item { margin: 1rem; }"}]
|
43
|
+
)
|
44
|
+
"""
|
45
|
+
|
46
|
+
def __init__(
|
47
|
+
self,
|
48
|
+
base_url: str,
|
49
|
+
log_config: Dict,
|
50
|
+
auth_config: Optional[Dict] = None,
|
51
|
+
browser_config: Optional[Dict] = None
|
52
|
+
):
|
53
|
+
"""
|
54
|
+
Initialize CursorFlow with environment configuration
|
55
|
+
|
56
|
+
Args:
|
57
|
+
base_url: "http://localhost:3000" or "https://staging.example.com"
|
58
|
+
log_config: {"source": "ssh|local|docker", "host": "...", "paths": [...]}
|
59
|
+
auth_config: {"method": "form", "username_selector": "#user", ...}
|
60
|
+
browser_config: {"headless": True, "debug_mode": False}
|
61
|
+
"""
|
62
|
+
self.base_url = base_url
|
63
|
+
self.log_config = log_config
|
64
|
+
self.auth_config = auth_config or {}
|
65
|
+
self.browser_config = browser_config or {"headless": True}
|
66
|
+
|
67
|
+
# Initialize core components
|
68
|
+
self.browser = BrowserController(base_url, self.browser_config)
|
69
|
+
self.log_collector = LogCollector(log_config)
|
70
|
+
self.correlator = EventCorrelator()
|
71
|
+
self.auth_handler = AuthHandler(auth_config) if auth_config else None
|
72
|
+
self.css_iterator = CSSIterator()
|
73
|
+
self.cursor_integration = CursorIntegration()
|
74
|
+
|
75
|
+
# Session tracking
|
76
|
+
self.session_id = None
|
77
|
+
self.timeline = []
|
78
|
+
self.artifacts = {"screenshots": [], "console_logs": [], "server_logs": []}
|
79
|
+
|
80
|
+
# Persistent session support for hot reload
|
81
|
+
self.persistent_session: Optional[PersistentSession] = None
|
82
|
+
self.session_manager = get_session_manager()
|
83
|
+
|
84
|
+
self.logger = logging.getLogger(__name__)
|
85
|
+
|
86
|
+
# Check for updates on initialization (background task)
|
87
|
+
self._check_for_updates_async()
|
88
|
+
|
89
|
+
async def execute_and_collect(
|
90
|
+
self,
|
91
|
+
actions: List[Dict],
|
92
|
+
session_options: Optional[Dict] = None
|
93
|
+
) -> Dict[str, Any]:
|
94
|
+
"""
|
95
|
+
Execute UI actions and collect all correlated data
|
96
|
+
|
97
|
+
Args:
|
98
|
+
actions: [
|
99
|
+
{"navigate": "/dashboard"},
|
100
|
+
{"click": "#refresh-button"},
|
101
|
+
{"screenshot": "after-click"}
|
102
|
+
]
|
103
|
+
session_options: {
|
104
|
+
"reuse_session": True,
|
105
|
+
"save_session": True,
|
106
|
+
"fresh_session": False
|
107
|
+
}
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
{
|
111
|
+
"success": bool,
|
112
|
+
"session_id": str,
|
113
|
+
"timeline": [{"time": timestamp, "type": "browser|server", "event": "...", ...}],
|
114
|
+
"correlations": [{"browser_event": "...", "server_event": "...", "confidence": 0.95}],
|
115
|
+
"artifacts": {
|
116
|
+
"screenshots": ["before.png", "after.png"],
|
117
|
+
"console_logs": [...],
|
118
|
+
"server_logs": [...]
|
119
|
+
}
|
120
|
+
}
|
121
|
+
"""
|
122
|
+
session_options = session_options or {}
|
123
|
+
start_time = time.time()
|
124
|
+
|
125
|
+
try:
|
126
|
+
# Initialize session
|
127
|
+
await self._initialize_session(session_options)
|
128
|
+
|
129
|
+
# Start monitoring
|
130
|
+
await self.log_collector.start_monitoring()
|
131
|
+
|
132
|
+
# Execute actions
|
133
|
+
success = await self._execute_actions(actions)
|
134
|
+
|
135
|
+
# Stop monitoring and collect data
|
136
|
+
server_logs = await self.log_collector.stop_monitoring()
|
137
|
+
|
138
|
+
# Organize timeline (NO analysis - just data organization)
|
139
|
+
organized_timeline = self.correlator.organize_timeline(
|
140
|
+
self.timeline, server_logs
|
141
|
+
)
|
142
|
+
summary = self.correlator.get_summary(organized_timeline)
|
143
|
+
|
144
|
+
# Package results
|
145
|
+
results = {
|
146
|
+
"success": success,
|
147
|
+
"session_id": self.session_id,
|
148
|
+
"execution_time": time.time() - start_time,
|
149
|
+
"timeline": organized_timeline, # Organized chronological data
|
150
|
+
"browser_events": self.timeline, # Raw browser events
|
151
|
+
"server_logs": server_logs, # Raw server logs
|
152
|
+
"summary": summary, # Basic counts
|
153
|
+
"artifacts": self.artifacts
|
154
|
+
}
|
155
|
+
|
156
|
+
self.logger.info(f"Test execution completed: {success}, timeline events: {len(organized_timeline)}")
|
157
|
+
return results
|
158
|
+
|
159
|
+
except Exception as e:
|
160
|
+
self.logger.error(f"Test execution failed: {e}")
|
161
|
+
return {
|
162
|
+
"success": False,
|
163
|
+
"error": str(e),
|
164
|
+
"timeline": self.timeline,
|
165
|
+
"artifacts": self.artifacts
|
166
|
+
}
|
167
|
+
finally:
|
168
|
+
await self._cleanup_session(session_options)
|
169
|
+
|
170
|
+
async def css_iteration_session(
|
171
|
+
self,
|
172
|
+
base_actions: List[Dict],
|
173
|
+
css_changes: List[Dict],
|
174
|
+
viewport_configs: Optional[List[Dict]] = None
|
175
|
+
) -> Dict[str, Any]:
|
176
|
+
"""
|
177
|
+
Rapid CSS iteration with visual feedback
|
178
|
+
|
179
|
+
Args:
|
180
|
+
base_actions: [
|
181
|
+
{"navigate": "/page"},
|
182
|
+
{"wait_for": "#main-content"},
|
183
|
+
{"screenshot": "baseline"}
|
184
|
+
]
|
185
|
+
css_changes: [
|
186
|
+
{
|
187
|
+
"name": "flex-spacing-fix",
|
188
|
+
"css": ".container { display: flex; gap: 1rem; }",
|
189
|
+
"rationale": "Fix spacing between items"
|
190
|
+
}
|
191
|
+
]
|
192
|
+
viewport_configs: [
|
193
|
+
{"width": 1440, "height": 900, "name": "desktop"},
|
194
|
+
{"width": 768, "height": 1024, "name": "tablet"}
|
195
|
+
]
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
{
|
199
|
+
"baseline": {
|
200
|
+
"screenshot": "baseline.png",
|
201
|
+
"computed_styles": {...},
|
202
|
+
"layout_metrics": {...}
|
203
|
+
},
|
204
|
+
"iterations": [
|
205
|
+
{
|
206
|
+
"name": "flex-spacing-fix",
|
207
|
+
"screenshot": "iteration_1.png",
|
208
|
+
"diff_image": "diff_1.png",
|
209
|
+
"layout_changes": [...],
|
210
|
+
"console_errors": [...],
|
211
|
+
"performance_impact": {...}
|
212
|
+
}
|
213
|
+
]
|
214
|
+
}
|
215
|
+
"""
|
216
|
+
try:
|
217
|
+
# Initialize for CSS iteration
|
218
|
+
await self.browser.initialize()
|
219
|
+
|
220
|
+
# Execute base actions and capture baseline
|
221
|
+
await self._execute_actions(base_actions)
|
222
|
+
baseline = await self.css_iterator.capture_baseline(self.browser.page)
|
223
|
+
|
224
|
+
# Iterate through CSS changes
|
225
|
+
iterations = []
|
226
|
+
for i, css_change in enumerate(css_changes):
|
227
|
+
iteration_result = await self.css_iterator.apply_css_and_capture(
|
228
|
+
self.browser.page, css_change, baseline
|
229
|
+
)
|
230
|
+
iterations.append(iteration_result)
|
231
|
+
|
232
|
+
# Test across viewports if specified
|
233
|
+
if viewport_configs:
|
234
|
+
for viewport in viewport_configs:
|
235
|
+
await self.browser.set_viewport(viewport["width"], viewport["height"])
|
236
|
+
viewport_baseline = await self.css_iterator.capture_baseline(self.browser.page)
|
237
|
+
|
238
|
+
for css_change in css_changes:
|
239
|
+
viewport_iteration = await self.css_iterator.apply_css_and_capture(
|
240
|
+
self.browser.page, css_change, viewport_baseline,
|
241
|
+
suffix=f"_{viewport['name']}"
|
242
|
+
)
|
243
|
+
iterations.append(viewport_iteration)
|
244
|
+
|
245
|
+
# Create raw results
|
246
|
+
raw_results = {
|
247
|
+
"baseline": baseline,
|
248
|
+
"iterations": iterations,
|
249
|
+
"summary": {
|
250
|
+
"total_changes": len(css_changes),
|
251
|
+
"viewports_tested": len(viewport_configs) if viewport_configs else 1,
|
252
|
+
"recommended_iteration": self._recommend_best_iteration(iterations)
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
# Format for Cursor with session management and actionable insights
|
257
|
+
cursor_results = self.cursor_integration.format_css_iteration_results(
|
258
|
+
raw_results=raw_results,
|
259
|
+
session_id=self.session_id,
|
260
|
+
project_context={
|
261
|
+
"framework": "auto-detected", # Could be enhanced with real detection
|
262
|
+
"base_url": self.base_url,
|
263
|
+
"test_type": "css_iteration"
|
264
|
+
}
|
265
|
+
)
|
266
|
+
|
267
|
+
return cursor_results
|
268
|
+
|
269
|
+
except Exception as e:
|
270
|
+
self.logger.error(f"CSS iteration failed: {e}")
|
271
|
+
return {"success": False, "error": str(e)}
|
272
|
+
finally:
|
273
|
+
await self.browser.cleanup()
|
274
|
+
|
275
|
+
async def css_iteration_persistent(
|
276
|
+
self,
|
277
|
+
base_actions: List[Dict],
|
278
|
+
css_changes: List[Dict],
|
279
|
+
session_options: Optional[Dict] = None
|
280
|
+
) -> Dict[str, Any]:
|
281
|
+
"""
|
282
|
+
CSS iteration using persistent sessions for hot reload environments
|
283
|
+
|
284
|
+
Maintains browser state between iterations, taking advantage of hot reload
|
285
|
+
capabilities for faster CSS iteration cycles.
|
286
|
+
|
287
|
+
Args:
|
288
|
+
base_actions: Initial actions to set up the page
|
289
|
+
css_changes: List of CSS changes to test
|
290
|
+
session_options: {
|
291
|
+
"session_id": "optional-custom-id",
|
292
|
+
"reuse_session": True,
|
293
|
+
"hot_reload": True,
|
294
|
+
"keep_session_alive": True
|
295
|
+
}
|
296
|
+
|
297
|
+
Returns:
|
298
|
+
Enhanced results with session information and hot reload data
|
299
|
+
"""
|
300
|
+
session_options = session_options or {}
|
301
|
+
start_time = time.time()
|
302
|
+
|
303
|
+
try:
|
304
|
+
# Get or create persistent session
|
305
|
+
session_id = session_options.get("session_id", f"css_session_{int(time.time())}")
|
306
|
+
self.persistent_session = await self.session_manager.get_or_create_session(
|
307
|
+
session_id=session_id,
|
308
|
+
base_url=self.base_url,
|
309
|
+
config={
|
310
|
+
**self.browser_config,
|
311
|
+
"hot_reload_enabled": session_options.get("hot_reload", True),
|
312
|
+
"keep_alive": session_options.get("keep_session_alive", True)
|
313
|
+
}
|
314
|
+
)
|
315
|
+
|
316
|
+
# Initialize persistent session
|
317
|
+
session_initialized = await self.persistent_session.initialize()
|
318
|
+
if not session_initialized:
|
319
|
+
return {"success": False, "error": "Failed to initialize persistent session"}
|
320
|
+
|
321
|
+
# Execute base actions if this is a new session or explicitly requested
|
322
|
+
if (not session_options.get("reuse_session", True) or
|
323
|
+
not self.persistent_session.baseline_captured):
|
324
|
+
|
325
|
+
self.logger.info("Executing base actions for CSS iteration setup")
|
326
|
+
await self._execute_persistent_actions(base_actions)
|
327
|
+
self.persistent_session.baseline_captured = True
|
328
|
+
|
329
|
+
# Capture baseline state
|
330
|
+
baseline = await self._capture_persistent_baseline()
|
331
|
+
|
332
|
+
# Perform CSS iterations with persistent session
|
333
|
+
iterations = []
|
334
|
+
|
335
|
+
for i, css_change in enumerate(css_changes):
|
336
|
+
self.logger.info(f"Applying CSS iteration {i+1}/{len(css_changes)}: {css_change.get('name', 'unnamed')}")
|
337
|
+
|
338
|
+
# Apply CSS using persistent session (with hot reload when available)
|
339
|
+
iteration_result = await self.persistent_session.apply_css_persistent(
|
340
|
+
css=css_change.get("css", ""),
|
341
|
+
name=css_change.get("name", f"iteration_{i+1}"),
|
342
|
+
replace_previous=css_change.get("replace_previous", False)
|
343
|
+
)
|
344
|
+
|
345
|
+
# Enhance with CursorFlow iteration data
|
346
|
+
if iteration_result.get("success"):
|
347
|
+
enhanced_result = await self._enhance_iteration_result(
|
348
|
+
iteration_result, css_change, baseline
|
349
|
+
)
|
350
|
+
iterations.append(enhanced_result)
|
351
|
+
else:
|
352
|
+
iterations.append(iteration_result)
|
353
|
+
|
354
|
+
# Small delay to let changes settle
|
355
|
+
await asyncio.sleep(0.1)
|
356
|
+
|
357
|
+
# Get session information
|
358
|
+
session_info = await self.persistent_session.get_session_info()
|
359
|
+
|
360
|
+
# Create results
|
361
|
+
results = {
|
362
|
+
"success": True,
|
363
|
+
"session_id": session_id,
|
364
|
+
"execution_time": time.time() - start_time,
|
365
|
+
"baseline": baseline,
|
366
|
+
"iterations": iterations,
|
367
|
+
"session_info": session_info,
|
368
|
+
"hot_reload_used": session_info.get("hot_reload_available", False),
|
369
|
+
"total_iterations": len(iterations),
|
370
|
+
"summary": {
|
371
|
+
"successful_iterations": len([i for i in iterations if i.get("success", False)]),
|
372
|
+
"failed_iterations": len([i for i in iterations if not i.get("success", True)]),
|
373
|
+
"hot_reload_available": session_info.get("hot_reload_available", False),
|
374
|
+
"session_reused": session_options.get("reuse_session", True),
|
375
|
+
"recommended_iteration": self._recommend_best_iteration(iterations)
|
376
|
+
}
|
377
|
+
}
|
378
|
+
|
379
|
+
# Format for Cursor integration
|
380
|
+
cursor_results = self.cursor_integration.format_persistent_css_results(
|
381
|
+
results,
|
382
|
+
{"framework": "auto-detected", "hot_reload": True}
|
383
|
+
)
|
384
|
+
|
385
|
+
# Keep session alive if requested
|
386
|
+
if not session_options.get("keep_session_alive", True):
|
387
|
+
await self.persistent_session.cleanup(save_state=True)
|
388
|
+
self.persistent_session = None
|
389
|
+
|
390
|
+
return cursor_results
|
391
|
+
|
392
|
+
except Exception as e:
|
393
|
+
self.logger.error(f"Persistent CSS iteration failed: {e}")
|
394
|
+
return {
|
395
|
+
"success": False,
|
396
|
+
"error": str(e),
|
397
|
+
"session_id": session_id if 'session_id' in locals() else None
|
398
|
+
}
|
399
|
+
|
400
|
+
async def _execute_persistent_actions(self, actions: List[Dict]) -> bool:
|
401
|
+
"""Execute actions using persistent session"""
|
402
|
+
try:
|
403
|
+
for action in actions:
|
404
|
+
if "navigate" in action:
|
405
|
+
path = action["navigate"]
|
406
|
+
if isinstance(path, dict):
|
407
|
+
path = path["url"]
|
408
|
+
await self.persistent_session.navigate_persistent(path)
|
409
|
+
|
410
|
+
elif "wait_for" in action:
|
411
|
+
selector = action["wait_for"]
|
412
|
+
if isinstance(selector, dict):
|
413
|
+
selector = selector["selector"]
|
414
|
+
await self.persistent_session.browser.wait_for_element(selector)
|
415
|
+
|
416
|
+
elif "screenshot" in action:
|
417
|
+
name = action["screenshot"]
|
418
|
+
await self.persistent_session.browser.screenshot(name)
|
419
|
+
|
420
|
+
# Add other actions as needed
|
421
|
+
return True
|
422
|
+
|
423
|
+
except Exception as e:
|
424
|
+
self.logger.error(f"Persistent action execution failed: {e}")
|
425
|
+
return False
|
426
|
+
|
427
|
+
async def _capture_persistent_baseline(self) -> Dict[str, Any]:
|
428
|
+
"""Capture baseline using persistent session"""
|
429
|
+
if not self.persistent_session or not self.persistent_session.browser:
|
430
|
+
return {}
|
431
|
+
|
432
|
+
try:
|
433
|
+
# Use CSS iterator for baseline capture
|
434
|
+
baseline = await self.css_iterator.capture_baseline(self.persistent_session.browser.page)
|
435
|
+
|
436
|
+
# Enhance with persistent session data
|
437
|
+
session_state = await self.persistent_session._capture_session_state("baseline")
|
438
|
+
baseline.update({
|
439
|
+
"session_state": session_state,
|
440
|
+
"hot_reload_detected": await self.persistent_session._check_hot_reload_capability(),
|
441
|
+
"iteration_context": {
|
442
|
+
"session_id": self.persistent_session.session_id,
|
443
|
+
"previous_iterations": self.persistent_session.iteration_count
|
444
|
+
}
|
445
|
+
})
|
446
|
+
|
447
|
+
return baseline
|
448
|
+
|
449
|
+
except Exception as e:
|
450
|
+
self.logger.error(f"Persistent baseline capture failed: {e}")
|
451
|
+
return {"error": str(e)}
|
452
|
+
|
453
|
+
async def _enhance_iteration_result(
|
454
|
+
self,
|
455
|
+
iteration_result: Dict,
|
456
|
+
css_change: Dict,
|
457
|
+
baseline: Dict
|
458
|
+
) -> Dict[str, Any]:
|
459
|
+
"""Enhance iteration result with CursorFlow analysis data"""
|
460
|
+
try:
|
461
|
+
enhanced = iteration_result.copy()
|
462
|
+
|
463
|
+
# Add CSS iterator analysis
|
464
|
+
if self.persistent_session and self.persistent_session.browser:
|
465
|
+
css_analysis = await self.css_iterator.apply_css_and_capture(
|
466
|
+
page=self.persistent_session.browser.page,
|
467
|
+
css_change=css_change,
|
468
|
+
baseline=baseline,
|
469
|
+
suffix="_persistent"
|
470
|
+
)
|
471
|
+
|
472
|
+
# Merge analysis data
|
473
|
+
enhanced.update({
|
474
|
+
"css_analysis": css_analysis,
|
475
|
+
"visual_comparison": css_analysis.get("changes", {}),
|
476
|
+
"performance_impact": css_analysis.get("performance_metrics", {}),
|
477
|
+
"console_errors": css_analysis.get("console_errors", [])
|
478
|
+
})
|
479
|
+
|
480
|
+
# Add persistent session context
|
481
|
+
enhanced.update({
|
482
|
+
"iteration_method": iteration_result.get("method", "standard"),
|
483
|
+
"hot_reload_used": iteration_result.get("method") == "hot_reload",
|
484
|
+
"session_persistent": True
|
485
|
+
})
|
486
|
+
|
487
|
+
return enhanced
|
488
|
+
|
489
|
+
except Exception as e:
|
490
|
+
self.logger.error(f"Failed to enhance iteration result: {e}")
|
491
|
+
return iteration_result
|
492
|
+
|
493
|
+
async def get_persistent_session_info(self) -> Optional[Dict[str, Any]]:
|
494
|
+
"""Get information about current persistent session"""
|
495
|
+
if self.persistent_session:
|
496
|
+
return await self.persistent_session.get_session_info()
|
497
|
+
return None
|
498
|
+
|
499
|
+
async def cleanup_persistent_session(self, save_state: bool = True):
|
500
|
+
"""Clean up current persistent session"""
|
501
|
+
if self.persistent_session:
|
502
|
+
await self.persistent_session.cleanup(save_state=save_state)
|
503
|
+
self.persistent_session = None
|
504
|
+
|
505
|
+
async def _initialize_session(self, session_options: Dict):
|
506
|
+
"""Initialize browser and authentication session"""
|
507
|
+
self.session_id = f"session_{int(time.time())}"
|
508
|
+
|
509
|
+
# Initialize browser
|
510
|
+
await self.browser.initialize()
|
511
|
+
|
512
|
+
# Handle authentication
|
513
|
+
if self.auth_handler and not session_options.get("skip_auth", False):
|
514
|
+
await self.auth_handler.authenticate(
|
515
|
+
self.browser.page,
|
516
|
+
session_options
|
517
|
+
)
|
518
|
+
|
519
|
+
async def _execute_actions(self, actions: List[Dict]) -> bool:
|
520
|
+
"""Execute list of declarative actions"""
|
521
|
+
try:
|
522
|
+
for action in actions:
|
523
|
+
await self._execute_single_action(action)
|
524
|
+
return True
|
525
|
+
except Exception as e:
|
526
|
+
self.logger.error(f"Action execution failed: {e}")
|
527
|
+
return False
|
528
|
+
|
529
|
+
async def _execute_single_action(self, action: Dict):
|
530
|
+
"""Execute a single declarative action and track it"""
|
531
|
+
action_start = time.time()
|
532
|
+
|
533
|
+
try:
|
534
|
+
# Navigation actions
|
535
|
+
if "navigate" in action:
|
536
|
+
url = action["navigate"]
|
537
|
+
if isinstance(url, dict):
|
538
|
+
url = url["url"]
|
539
|
+
await self.browser.navigate(url)
|
540
|
+
|
541
|
+
# Interaction actions
|
542
|
+
elif "click" in action:
|
543
|
+
selector = action["click"]
|
544
|
+
if isinstance(selector, dict):
|
545
|
+
selector = selector["selector"]
|
546
|
+
await self.browser.click(selector)
|
547
|
+
|
548
|
+
elif "fill" in action:
|
549
|
+
config = action["fill"]
|
550
|
+
await self.browser.fill(config["selector"], config["value"])
|
551
|
+
|
552
|
+
elif "type" in action:
|
553
|
+
config = action["type"]
|
554
|
+
await self.browser.type(config["selector"], config["text"])
|
555
|
+
|
556
|
+
# Waiting actions
|
557
|
+
elif "wait" in action:
|
558
|
+
await asyncio.sleep(action["wait"])
|
559
|
+
|
560
|
+
elif "wait_for" in action:
|
561
|
+
selector = action["wait_for"]
|
562
|
+
if isinstance(selector, dict):
|
563
|
+
selector = selector["selector"]
|
564
|
+
await self.browser.wait_for_element(selector)
|
565
|
+
|
566
|
+
# Capture actions
|
567
|
+
elif "screenshot" in action:
|
568
|
+
name = action["screenshot"]
|
569
|
+
screenshot_path = await self.browser.screenshot(name)
|
570
|
+
self.artifacts["screenshots"].append(screenshot_path)
|
571
|
+
|
572
|
+
elif "authenticate" in action:
|
573
|
+
if self.auth_handler:
|
574
|
+
await self.auth_handler.authenticate(self.browser.page, action["authenticate"])
|
575
|
+
|
576
|
+
# Record action in timeline
|
577
|
+
self.timeline.append({
|
578
|
+
"timestamp": action_start,
|
579
|
+
"type": "browser",
|
580
|
+
"event": list(action.keys())[0],
|
581
|
+
"data": action,
|
582
|
+
"duration": time.time() - action_start
|
583
|
+
})
|
584
|
+
|
585
|
+
except Exception as e:
|
586
|
+
self.logger.error(f"Action failed: {action}, error: {e}")
|
587
|
+
raise
|
588
|
+
|
589
|
+
async def _cleanup_session(self, session_options: Dict):
|
590
|
+
"""Clean up browser session"""
|
591
|
+
try:
|
592
|
+
# Save session if requested
|
593
|
+
if session_options.get("save_session", False) and self.auth_handler:
|
594
|
+
await self.auth_handler.save_session(self.browser.page, self.session_id)
|
595
|
+
|
596
|
+
# Cleanup browser
|
597
|
+
await self.browser.cleanup()
|
598
|
+
|
599
|
+
except Exception as e:
|
600
|
+
self.logger.error(f"Session cleanup failed: {e}")
|
601
|
+
|
602
|
+
def _recommend_best_iteration(self, iterations: List[Dict]) -> Optional[str]:
|
603
|
+
"""Recommend best CSS iteration based on metrics"""
|
604
|
+
if not iterations:
|
605
|
+
return None
|
606
|
+
|
607
|
+
# Simple scoring based on lack of console errors and performance
|
608
|
+
best_iteration = None
|
609
|
+
best_score = -1
|
610
|
+
|
611
|
+
for iteration in iterations:
|
612
|
+
score = 0
|
613
|
+
|
614
|
+
# Penalty for console errors
|
615
|
+
if not iteration.get("console_errors", []):
|
616
|
+
score += 50
|
617
|
+
|
618
|
+
# Bonus for good performance
|
619
|
+
perf = iteration.get("performance_impact", {})
|
620
|
+
if perf.get("render_time", 1000) < 100:
|
621
|
+
score += 30
|
622
|
+
|
623
|
+
if score > best_score:
|
624
|
+
best_score = score
|
625
|
+
best_iteration = iteration.get("name")
|
626
|
+
|
627
|
+
return best_iteration
|
628
|
+
|
629
|
+
def _check_for_updates_async(self):
|
630
|
+
"""Check for updates in background (non-blocking)"""
|
631
|
+
try:
|
632
|
+
import asyncio
|
633
|
+
from ..auto_updater import check_for_updates_on_startup
|
634
|
+
|
635
|
+
# Try to run update check in background
|
636
|
+
try:
|
637
|
+
loop = asyncio.get_event_loop()
|
638
|
+
if loop.is_running():
|
639
|
+
# Schedule as background task
|
640
|
+
loop.create_task(check_for_updates_on_startup(str(Path.cwd())))
|
641
|
+
else:
|
642
|
+
# Create new loop for quick check
|
643
|
+
asyncio.run(check_for_updates_on_startup(str(Path.cwd())))
|
644
|
+
except Exception:
|
645
|
+
# If async fails, skip silently - updates not critical for operation
|
646
|
+
pass
|
647
|
+
except ImportError:
|
648
|
+
# Auto-updater not available, skip silently
|
649
|
+
pass
|