cursorflow 2.0.3__py3-none-any.whl → 2.1.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 +24 -17
- cursorflow/cli.py +51 -15
- cursorflow/core/browser_controller.py +114 -13
- cursorflow/core/browser_engine.py +17 -4
- cursorflow/core/cursorflow.py +259 -8
- cursorflow/install_cursorflow_rules.py +4 -4
- cursorflow-2.1.0.dist-info/METADATA +350 -0
- {cursorflow-2.0.3.dist-info → cursorflow-2.1.0.dist-info}/RECORD +12 -12
- cursorflow-2.0.3.dist-info/METADATA +0 -293
- {cursorflow-2.0.3.dist-info → cursorflow-2.1.0.dist-info}/WHEEL +0 -0
- {cursorflow-2.0.3.dist-info → cursorflow-2.1.0.dist-info}/entry_points.txt +0 -0
- {cursorflow-2.0.3.dist-info → cursorflow-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-2.0.3.dist-info → cursorflow-2.1.0.dist-info}/top_level.txt +0 -0
cursorflow/__init__.py
CHANGED
@@ -18,38 +18,45 @@ from .core.log_monitor import LogMonitor
|
|
18
18
|
from .core.error_correlator import ErrorCorrelator
|
19
19
|
|
20
20
|
def _get_version():
|
21
|
-
"""Get version from
|
21
|
+
"""Get version from pyproject.toml or fallback to default"""
|
22
22
|
try:
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
23
|
+
# Try to read from pyproject.toml first (most reliable)
|
24
|
+
import tomllib
|
25
|
+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
26
|
+
if pyproject_path.exists():
|
27
|
+
with open(pyproject_path, 'rb') as f:
|
28
|
+
data = tomllib.load(f)
|
29
|
+
return data["project"]["version"]
|
30
|
+
except Exception:
|
31
|
+
pass
|
32
|
+
|
33
|
+
try:
|
34
|
+
# Try toml library as fallback (for older Python)
|
35
|
+
import toml
|
36
|
+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
37
|
+
if pyproject_path.exists():
|
38
|
+
with open(pyproject_path, 'r') as f:
|
39
|
+
data = toml.load(f)
|
40
|
+
return data["project"]["version"]
|
33
41
|
except Exception:
|
34
42
|
pass
|
35
43
|
|
36
44
|
try:
|
37
|
-
# Try
|
45
|
+
# Try git tag as final option
|
46
|
+
import subprocess
|
38
47
|
result = subprocess.run(
|
39
|
-
['git', 'describe', '--tags', '--
|
48
|
+
['git', 'describe', '--tags', '--exact-match'],
|
40
49
|
capture_output=True,
|
41
50
|
text=True,
|
42
51
|
cwd=Path(__file__).parent.parent
|
43
52
|
)
|
44
53
|
if result.returncode == 0:
|
45
|
-
|
46
|
-
# Add dev suffix if not on exact tag
|
47
|
-
return f"{tag}-dev"
|
54
|
+
return result.stdout.strip().lstrip('v')
|
48
55
|
except Exception:
|
49
56
|
pass
|
50
57
|
|
51
58
|
# Fallback version - should match pyproject.toml
|
52
|
-
return "2.0
|
59
|
+
return "2.1.0"
|
53
60
|
|
54
61
|
__version__ = _get_version()
|
55
62
|
__author__ = "GeekWarrior Development"
|
cursorflow/cli.py
CHANGED
@@ -46,7 +46,9 @@ def main():
|
|
46
46
|
help='Run browser in headless mode')
|
47
47
|
@click.option('--timeout', type=int, default=30,
|
48
48
|
help='Timeout in seconds for actions')
|
49
|
-
|
49
|
+
@click.option('--responsive', is_flag=True,
|
50
|
+
help='Test across multiple viewports (mobile, tablet, desktop)')
|
51
|
+
def test(base_url, path, actions, output, logs, config, verbose, headless, timeout, responsive):
|
50
52
|
"""Test UI flows and interactions with real-time log monitoring"""
|
51
53
|
|
52
54
|
if verbose:
|
@@ -113,25 +115,59 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
|
|
113
115
|
|
114
116
|
# Execute test actions
|
115
117
|
try:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
console.print(f"
|
118
|
+
if responsive:
|
119
|
+
# Define standard responsive viewports
|
120
|
+
viewports = [
|
121
|
+
{"width": 375, "height": 667, "name": "mobile"},
|
122
|
+
{"width": 768, "height": 1024, "name": "tablet"},
|
123
|
+
{"width": 1440, "height": 900, "name": "desktop"}
|
124
|
+
]
|
125
|
+
|
126
|
+
console.print(f"📱 Executing responsive test across {len(viewports)} viewports...")
|
127
|
+
console.print(f" 📱 Mobile: 375x667")
|
128
|
+
console.print(f" 📟 Tablet: 768x1024")
|
129
|
+
console.print(f" 💻 Desktop: 1440x900")
|
130
|
+
|
131
|
+
results = asyncio.run(flow.test_responsive(viewports, test_actions))
|
132
|
+
|
133
|
+
# Display responsive results
|
134
|
+
console.print(f"✅ Responsive test completed: {test_description}")
|
135
|
+
execution_summary = results.get('execution_summary', {})
|
136
|
+
console.print(f"📊 Viewports tested: {execution_summary.get('successful_viewports', 0)}/{execution_summary.get('total_viewports', 0)}")
|
137
|
+
console.print(f"⏱️ Total execution time: {execution_summary.get('execution_time', 0):.2f}s")
|
138
|
+
console.print(f"📸 Screenshots: {len(results.get('artifacts', {}).get('screenshots', []))}")
|
139
|
+
|
140
|
+
# Show viewport performance
|
141
|
+
responsive_analysis = results.get('responsive_analysis', {})
|
142
|
+
if 'performance_analysis' in responsive_analysis:
|
143
|
+
perf = responsive_analysis['performance_analysis']
|
144
|
+
console.print(f"🏃 Fastest: {perf.get('fastest_viewport')}")
|
145
|
+
console.print(f"🐌 Slowest: {perf.get('slowest_viewport')}")
|
146
|
+
else:
|
147
|
+
console.print(f"🚀 Executing {len(test_actions)} actions...")
|
148
|
+
results = asyncio.run(flow.execute_and_collect(test_actions))
|
149
|
+
|
150
|
+
console.print(f"✅ Test completed: {test_description}")
|
151
|
+
console.print(f"📊 Browser events: {len(results.get('browser_events', []))}")
|
152
|
+
console.print(f"📋 Server logs: {len(results.get('server_logs', []))}")
|
153
|
+
console.print(f"📸 Screenshots: {len(results.get('artifacts', {}).get('screenshots', []))}")
|
154
|
+
|
155
|
+
# Show correlations if found
|
156
|
+
timeline = results.get('organized_timeline', [])
|
157
|
+
if timeline:
|
158
|
+
console.print(f"⏰ Timeline events: {len(timeline)}")
|
128
159
|
|
129
160
|
# Save results to file for Cursor analysis
|
130
161
|
if not output:
|
131
|
-
# Auto-generate meaningful filename
|
162
|
+
# Auto-generate meaningful filename in .cursorflow/artifacts/
|
132
163
|
session_id = results.get('session_id', 'unknown')
|
133
164
|
path_part = path.replace('/', '_') if path else 'root'
|
134
|
-
|
165
|
+
|
166
|
+
# Ensure .cursorflow/artifacts directory exists
|
167
|
+
artifacts_dir = Path('.cursorflow/artifacts')
|
168
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
169
|
+
|
170
|
+
output = artifacts_dir / f"cursorflow_{path_part}_{session_id}.json"
|
135
171
|
|
136
172
|
with open(output, 'w') as f:
|
137
173
|
json.dump(results, f, indent=2, default=str)
|
@@ -398,24 +398,109 @@ class BrowserController:
|
|
398
398
|
self.logger.error(f"Condition wait failed: {condition}, {e}")
|
399
399
|
raise
|
400
400
|
|
401
|
-
async def screenshot(self, name: str,
|
402
|
-
"""
|
401
|
+
async def screenshot(self, name: str, options: Optional[Dict] = None, capture_comprehensive_data: bool = True) -> Dict[str, Any]:
|
402
|
+
"""
|
403
|
+
Take screenshot with comprehensive page analysis - universal
|
404
|
+
|
405
|
+
Args:
|
406
|
+
name: Screenshot name/identifier
|
407
|
+
options: Enhanced screenshot options {
|
408
|
+
"full_page": bool, # Capture full page (default: False)
|
409
|
+
"clip": { # Clip to specific region
|
410
|
+
"x": int, "y": int, # Top-left coordinates
|
411
|
+
"width": int, "height": int
|
412
|
+
} OR {
|
413
|
+
"selector": str # Clip to element bounding box
|
414
|
+
},
|
415
|
+
"mask": [str], # CSS selectors to hide/mask
|
416
|
+
"quality": int # JPEG quality 0-100 (default: 80)
|
417
|
+
}
|
418
|
+
capture_comprehensive_data: Whether to capture detailed page analysis
|
419
|
+
"""
|
403
420
|
try:
|
421
|
+
# Process options with defaults
|
422
|
+
screenshot_options = options or {}
|
423
|
+
full_page = screenshot_options.get("full_page", False)
|
424
|
+
clip_config = screenshot_options.get("clip")
|
425
|
+
mask_selectors = screenshot_options.get("mask", [])
|
426
|
+
quality = screenshot_options.get("quality", 80)
|
427
|
+
|
404
428
|
timestamp = int(time.time())
|
405
429
|
screenshot_filename = f".cursorflow/artifacts/screenshots/{name}_{timestamp}.png"
|
406
430
|
|
431
|
+
# Apply masking if requested (hide sensitive elements)
|
432
|
+
masked_elements = []
|
433
|
+
if mask_selectors:
|
434
|
+
for selector in mask_selectors:
|
435
|
+
try:
|
436
|
+
await self.page.add_style_tag(content=f"""
|
437
|
+
{selector} {{
|
438
|
+
visibility: hidden !important;
|
439
|
+
opacity: 0 !important;
|
440
|
+
}}
|
441
|
+
""")
|
442
|
+
masked_elements.append(selector)
|
443
|
+
self.logger.debug(f"Masked element: {selector}")
|
444
|
+
except Exception as e:
|
445
|
+
self.logger.warning(f"Failed to mask {selector}: {e}")
|
446
|
+
|
447
|
+
# Prepare screenshot parameters
|
448
|
+
screenshot_params = {
|
449
|
+
"path": screenshot_filename,
|
450
|
+
"full_page": full_page,
|
451
|
+
"quality": quality
|
452
|
+
}
|
453
|
+
|
454
|
+
# Handle clipping options
|
455
|
+
if clip_config:
|
456
|
+
if "selector" in clip_config:
|
457
|
+
# Clip to element bounding box
|
458
|
+
try:
|
459
|
+
element = await self.page.wait_for_selector(clip_config["selector"], timeout=5000)
|
460
|
+
if element:
|
461
|
+
bounding_box = await element.bounding_box()
|
462
|
+
if bounding_box:
|
463
|
+
screenshot_params["clip"] = bounding_box
|
464
|
+
self.logger.debug(f"Clipping to element {clip_config['selector']}: {bounding_box}")
|
465
|
+
else:
|
466
|
+
self.logger.warning(f"Element {clip_config['selector']} has no bounding box")
|
467
|
+
else:
|
468
|
+
self.logger.warning(f"Element {clip_config['selector']} not found for clipping")
|
469
|
+
except Exception as e:
|
470
|
+
self.logger.warning(f"Failed to clip to element {clip_config['selector']}: {e}")
|
471
|
+
|
472
|
+
elif all(key in clip_config for key in ["x", "y", "width", "height"]):
|
473
|
+
# Clip to specific coordinates
|
474
|
+
screenshot_params["clip"] = {
|
475
|
+
"x": clip_config["x"],
|
476
|
+
"y": clip_config["y"],
|
477
|
+
"width": clip_config["width"],
|
478
|
+
"height": clip_config["height"]
|
479
|
+
}
|
480
|
+
self.logger.debug(f"Clipping to coordinates: {screenshot_params['clip']}")
|
481
|
+
|
407
482
|
# Take the visual screenshot
|
408
|
-
await self.page.screenshot(
|
409
|
-
|
410
|
-
|
411
|
-
|
483
|
+
await self.page.screenshot(**screenshot_params)
|
484
|
+
|
485
|
+
# Remove masking styles
|
486
|
+
if masked_elements:
|
487
|
+
try:
|
488
|
+
for selector in masked_elements:
|
489
|
+
await self.page.add_style_tag(content=f"""
|
490
|
+
{selector} {{
|
491
|
+
visibility: visible !important;
|
492
|
+
opacity: 1 !important;
|
493
|
+
}}
|
494
|
+
""")
|
495
|
+
except Exception as e:
|
496
|
+
self.logger.warning(f"Failed to remove masking: {e}")
|
412
497
|
|
413
498
|
# Always return structured data for consistency
|
414
499
|
screenshot_data = {
|
415
500
|
"screenshot_path": screenshot_filename,
|
416
501
|
"timestamp": timestamp,
|
417
502
|
"name": name,
|
418
|
-
"
|
503
|
+
"options": screenshot_options,
|
419
504
|
"session_id": self.session_id,
|
420
505
|
"trace_info": self.trace_manager.get_trace_info() if self.trace_manager else None
|
421
506
|
}
|
@@ -515,16 +600,32 @@ class BrowserController:
|
|
515
600
|
raise
|
516
601
|
|
517
602
|
async def get_performance_metrics(self) -> Dict:
|
518
|
-
"""Get page performance metrics - universal"""
|
603
|
+
"""Get page performance metrics - universal with proper null handling"""
|
519
604
|
try:
|
520
605
|
metrics = await self.page.evaluate("""
|
521
606
|
() => {
|
607
|
+
// Helper function to safely calculate timing differences
|
608
|
+
const safeTiming = (end, start) => {
|
609
|
+
if (!end || !start || end === 0 || start === 0) return null;
|
610
|
+
const diff = end - start;
|
611
|
+
return diff >= 0 ? diff : null;
|
612
|
+
};
|
613
|
+
|
522
614
|
const perf = performance.getEntriesByType('navigation')[0];
|
615
|
+
const paint = performance.getEntriesByType('paint');
|
616
|
+
const lcp = performance.getEntriesByType('largest-contentful-paint')[0];
|
617
|
+
|
523
618
|
return {
|
524
|
-
loadTime: perf ? perf.loadEventEnd
|
525
|
-
domContentLoaded: perf ? perf.domContentLoadedEventEnd
|
526
|
-
firstPaint:
|
527
|
-
largestContentfulPaint:
|
619
|
+
loadTime: perf ? safeTiming(perf.loadEventEnd, perf.loadEventStart) : null,
|
620
|
+
domContentLoaded: perf ? safeTiming(perf.domContentLoadedEventEnd, perf.domContentLoadedEventStart) : null,
|
621
|
+
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || null,
|
622
|
+
largestContentfulPaint: lcp?.startTime || null,
|
623
|
+
_reliability: {
|
624
|
+
navigation_available: perf !== undefined,
|
625
|
+
paint_available: paint.length > 0,
|
626
|
+
lcp_available: lcp !== undefined,
|
627
|
+
note: "null values in headless mode are expected"
|
628
|
+
}
|
528
629
|
};
|
529
630
|
}
|
530
631
|
""")
|
@@ -533,7 +634,7 @@ class BrowserController:
|
|
533
634
|
|
534
635
|
except Exception as e:
|
535
636
|
self.logger.error(f"Performance metrics failed: {e}")
|
536
|
-
return {}
|
637
|
+
return {"error": str(e)}
|
537
638
|
|
538
639
|
async def cleanup(self):
|
539
640
|
"""Clean up browser resources and stop trace recording"""
|
@@ -340,22 +340,35 @@ class BrowserEngine:
|
|
340
340
|
return validation_result
|
341
341
|
|
342
342
|
async def get_performance_metrics(self) -> Dict:
|
343
|
-
"""Get browser performance metrics"""
|
343
|
+
"""Get browser performance metrics with proper null handling"""
|
344
344
|
|
345
345
|
metrics = await self.page.evaluate("""() => {
|
346
|
+
// Helper function to safely calculate timing differences
|
347
|
+
const safeTiming = (end, start) => {
|
348
|
+
if (!end || !start || end === 0 || start === 0) return null;
|
349
|
+
const diff = end - start;
|
350
|
+
return diff >= 0 ? diff : null;
|
351
|
+
};
|
352
|
+
|
346
353
|
const timing = performance.timing;
|
347
354
|
const navigation = performance.getEntriesByType('navigation')[0];
|
348
355
|
|
349
356
|
return {
|
350
|
-
page_load_time: timing.loadEventEnd
|
351
|
-
dom_ready_time: timing.domContentLoadedEventEnd
|
357
|
+
page_load_time: safeTiming(timing.loadEventEnd, timing.navigationStart),
|
358
|
+
dom_ready_time: safeTiming(timing.domContentLoadedEventEnd, timing.navigationStart),
|
352
359
|
first_paint: navigation ? navigation.loadEventEnd : null,
|
353
360
|
resource_count: performance.getEntriesByType('resource').length,
|
354
361
|
memory_usage: performance.memory ? {
|
355
362
|
used: performance.memory.usedJSHeapSize,
|
356
363
|
total: performance.memory.totalJSHeapSize,
|
357
364
|
limit: performance.memory.jsHeapSizeLimit
|
358
|
-
} : null
|
365
|
+
} : null,
|
366
|
+
_reliability: {
|
367
|
+
timing_available: timing !== undefined,
|
368
|
+
navigation_available: navigation !== undefined,
|
369
|
+
memory_available: performance.memory !== undefined,
|
370
|
+
note: "null values in headless mode are expected"
|
371
|
+
}
|
359
372
|
};
|
360
373
|
}""")
|
361
374
|
|
cursorflow/core/cursorflow.py
CHANGED
@@ -716,9 +716,23 @@ class CursorFlow:
|
|
716
716
|
|
717
717
|
# Capture actions
|
718
718
|
elif "screenshot" in action:
|
719
|
-
|
720
|
-
|
721
|
-
|
719
|
+
screenshot_config = action["screenshot"]
|
720
|
+
|
721
|
+
# Handle both string and dict formats
|
722
|
+
if isinstance(screenshot_config, str):
|
723
|
+
# Simple format: {"screenshot": "name"}
|
724
|
+
name = screenshot_config
|
725
|
+
options = None
|
726
|
+
elif isinstance(screenshot_config, dict):
|
727
|
+
# Enhanced format: {"screenshot": {"name": "test", "options": {...}}}
|
728
|
+
name = screenshot_config.get("name", "screenshot")
|
729
|
+
options = screenshot_config.get("options")
|
730
|
+
else:
|
731
|
+
name = "screenshot"
|
732
|
+
options = None
|
733
|
+
|
734
|
+
screenshot_data = await self.browser.screenshot(name, options)
|
735
|
+
self.artifacts["screenshots"].append(screenshot_data)
|
722
736
|
|
723
737
|
elif "authenticate" in action:
|
724
738
|
if self.auth_handler:
|
@@ -750,12 +764,249 @@ class CursorFlow:
|
|
750
764
|
except Exception as e:
|
751
765
|
self.logger.error(f"Session cleanup failed: {e}")
|
752
766
|
|
753
|
-
def
|
754
|
-
"""
|
755
|
-
|
756
|
-
|
767
|
+
async def test_responsive(self, viewports: List[Dict], actions: List[Dict], session_options: Optional[Dict] = None) -> Dict[str, Any]:
|
768
|
+
"""
|
769
|
+
Test the same actions across multiple viewports in parallel
|
770
|
+
|
771
|
+
Args:
|
772
|
+
viewports: List of viewport configurations [{"width": 375, "height": 667, "name": "mobile"}, ...]
|
773
|
+
actions: Actions to execute on each viewport
|
774
|
+
session_options: Optional session configuration
|
775
|
+
|
776
|
+
Returns:
|
777
|
+
Dict with results for each viewport plus comparison analysis
|
778
|
+
"""
|
779
|
+
session_options = session_options or {}
|
780
|
+
responsive_session_id = session_options.get("session_id", f"responsive_{int(time.time())}")
|
781
|
+
|
782
|
+
self.logger.info(f"Starting responsive testing across {len(viewports)} viewports")
|
783
|
+
|
784
|
+
try:
|
785
|
+
# Execute tests in parallel across all viewports
|
786
|
+
viewport_tasks = []
|
787
|
+
for viewport in viewports:
|
788
|
+
task = self._test_single_viewport(viewport, actions, responsive_session_id)
|
789
|
+
viewport_tasks.append(task)
|
790
|
+
|
791
|
+
# Wait for all viewport tests to complete
|
792
|
+
viewport_results = await asyncio.gather(*viewport_tasks, return_exceptions=True)
|
793
|
+
|
794
|
+
# Process results and handle any exceptions
|
795
|
+
processed_results = {}
|
796
|
+
successful_viewports = []
|
797
|
+
failed_viewports = []
|
798
|
+
|
799
|
+
for i, result in enumerate(viewport_results):
|
800
|
+
viewport_name = viewports[i]["name"]
|
801
|
+
if isinstance(result, Exception):
|
802
|
+
self.logger.error(f"Viewport {viewport_name} failed: {result}")
|
803
|
+
failed_viewports.append({"name": viewport_name, "error": str(result)})
|
804
|
+
else:
|
805
|
+
processed_results[viewport_name] = result
|
806
|
+
successful_viewports.append(viewport_name)
|
807
|
+
|
808
|
+
# Create responsive analysis
|
809
|
+
responsive_analysis = self._analyze_responsive_results(processed_results, viewports)
|
810
|
+
|
811
|
+
return {
|
812
|
+
"session_id": responsive_session_id,
|
813
|
+
"timestamp": time.time(),
|
814
|
+
"viewport_results": processed_results,
|
815
|
+
"responsive_analysis": responsive_analysis,
|
816
|
+
"execution_summary": {
|
817
|
+
"total_viewports": len(viewports),
|
818
|
+
"successful_viewports": len(successful_viewports),
|
819
|
+
"failed_viewports": len(failed_viewports),
|
820
|
+
"success_rate": len(successful_viewports) / len(viewports),
|
821
|
+
"execution_time": responsive_analysis.get("total_execution_time", 0)
|
822
|
+
},
|
823
|
+
"failed_viewports": failed_viewports if failed_viewports else None,
|
824
|
+
"artifacts": {
|
825
|
+
"screenshots": self._collect_responsive_screenshots(processed_results),
|
826
|
+
"comprehensive_data": self._collect_responsive_data(processed_results)
|
827
|
+
}
|
828
|
+
}
|
829
|
+
|
830
|
+
except Exception as e:
|
831
|
+
self.logger.error(f"Responsive testing failed: {e}")
|
832
|
+
return {
|
833
|
+
"session_id": responsive_session_id,
|
834
|
+
"error": str(e),
|
835
|
+
"timestamp": time.time()
|
836
|
+
}
|
837
|
+
|
838
|
+
async def _test_single_viewport(self, viewport: Dict, actions: List[Dict], session_id: str) -> Dict[str, Any]:
|
839
|
+
"""Execute test actions for a single viewport"""
|
840
|
+
viewport_name = viewport["name"]
|
841
|
+
viewport_width = viewport["width"]
|
842
|
+
viewport_height = viewport["height"]
|
843
|
+
|
844
|
+
# Create a separate browser instance for this viewport
|
845
|
+
viewport_browser = BrowserController(
|
846
|
+
base_url=self.base_url,
|
847
|
+
config={
|
848
|
+
**self.browser_config,
|
849
|
+
"viewport": {"width": viewport_width, "height": viewport_height}
|
850
|
+
}
|
851
|
+
)
|
852
|
+
|
853
|
+
try:
|
854
|
+
# Initialize browser with specific viewport
|
855
|
+
await viewport_browser.initialize()
|
856
|
+
|
857
|
+
# Set the session ID for artifact organization
|
858
|
+
viewport_browser.session_id = f"{session_id}_{viewport_name}"
|
859
|
+
|
860
|
+
# Execute all actions for this viewport
|
861
|
+
viewport_artifacts = {"screenshots": [], "comprehensive_data": []}
|
862
|
+
viewport_timeline = []
|
863
|
+
|
864
|
+
for action in actions:
|
865
|
+
action_start = time.time()
|
866
|
+
|
867
|
+
# Use the same action handling as execute_and_collect for consistency
|
868
|
+
if "navigate" in action:
|
869
|
+
await viewport_browser.navigate(action["navigate"])
|
870
|
+
elif "click" in action:
|
871
|
+
selector = action["click"]
|
872
|
+
if isinstance(selector, dict):
|
873
|
+
selector = selector["selector"]
|
874
|
+
await viewport_browser.click(selector)
|
875
|
+
elif "fill" in action:
|
876
|
+
fill_config = action["fill"]
|
877
|
+
await viewport_browser.fill(fill_config["selector"], fill_config["value"])
|
878
|
+
elif "wait_for" in action:
|
879
|
+
selector = action["wait_for"]
|
880
|
+
if isinstance(selector, dict):
|
881
|
+
selector = selector["selector"]
|
882
|
+
await viewport_browser.wait_for_element(selector)
|
883
|
+
elif "wait" in action:
|
884
|
+
wait_time = action["wait"] * 1000 # Convert to milliseconds
|
885
|
+
await viewport_browser.page.wait_for_timeout(wait_time)
|
886
|
+
elif "screenshot" in action:
|
887
|
+
screenshot_config = action["screenshot"]
|
888
|
+
|
889
|
+
# Handle both string and dict formats
|
890
|
+
if isinstance(screenshot_config, str):
|
891
|
+
name = f"{viewport_name}_{screenshot_config}"
|
892
|
+
options = None
|
893
|
+
elif isinstance(screenshot_config, dict):
|
894
|
+
name = f"{viewport_name}_{screenshot_config.get('name', 'screenshot')}"
|
895
|
+
options = screenshot_config.get("options")
|
896
|
+
else:
|
897
|
+
name = f"{viewport_name}_screenshot"
|
898
|
+
options = None
|
899
|
+
|
900
|
+
screenshot_data = await viewport_browser.screenshot(name, options)
|
901
|
+
viewport_artifacts["screenshots"].append(screenshot_data)
|
902
|
+
elif "authenticate" in action:
|
903
|
+
# Note: Auth handler would need to be passed to viewport browser
|
904
|
+
# For now, log that auth is not supported in responsive mode
|
905
|
+
self.logger.warning(f"Authentication action skipped in responsive mode for viewport {viewport_name}")
|
906
|
+
else:
|
907
|
+
# Log unsupported actions
|
908
|
+
action_type = list(action.keys())[0] if action else "unknown"
|
909
|
+
self.logger.warning(f"Unsupported action '{action_type}' in responsive mode for viewport {viewport_name}")
|
910
|
+
|
911
|
+
# Record action in timeline
|
912
|
+
viewport_timeline.append({
|
913
|
+
"timestamp": action_start,
|
914
|
+
"type": "browser",
|
915
|
+
"event": list(action.keys())[0],
|
916
|
+
"data": action,
|
917
|
+
"duration": time.time() - action_start,
|
918
|
+
"viewport": viewport_name
|
919
|
+
})
|
757
920
|
|
758
|
-
|
921
|
+
return {
|
922
|
+
"viewport": viewport,
|
923
|
+
"artifacts": viewport_artifacts,
|
924
|
+
"timeline": viewport_timeline,
|
925
|
+
"success": True,
|
926
|
+
"execution_time": sum(event["duration"] for event in viewport_timeline)
|
927
|
+
}
|
928
|
+
|
929
|
+
except Exception as e:
|
930
|
+
self.logger.error(f"Viewport {viewport_name} test failed: {e}")
|
931
|
+
return {
|
932
|
+
"viewport": viewport,
|
933
|
+
"error": str(e),
|
934
|
+
"success": False
|
935
|
+
}
|
936
|
+
finally:
|
937
|
+
await viewport_browser.cleanup()
|
938
|
+
|
939
|
+
def _analyze_responsive_results(self, results: Dict, viewports: List[Dict]) -> Dict[str, Any]:
|
940
|
+
"""Analyze responsive testing results for patterns and insights"""
|
941
|
+
if not results:
|
942
|
+
return {"error": "No successful viewport results to analyze"}
|
943
|
+
|
944
|
+
analysis = {
|
945
|
+
"viewport_comparison": {},
|
946
|
+
"responsive_insights": [],
|
947
|
+
"performance_analysis": {},
|
948
|
+
"total_execution_time": 0
|
949
|
+
}
|
950
|
+
|
951
|
+
# Compare viewports
|
952
|
+
viewport_names = list(results.keys())
|
953
|
+
for viewport_name in viewport_names:
|
954
|
+
result = results[viewport_name]
|
955
|
+
viewport_config = next(v for v in viewports if v["name"] == viewport_name)
|
956
|
+
|
957
|
+
analysis["viewport_comparison"][viewport_name] = {
|
958
|
+
"dimensions": f"{viewport_config['width']}x{viewport_config['height']}",
|
959
|
+
"screenshot_count": len(result.get("artifacts", {}).get("screenshots", [])),
|
960
|
+
"execution_time": result.get("execution_time", 0),
|
961
|
+
"actions_completed": len(result.get("timeline", [])),
|
962
|
+
"success": result.get("success", False)
|
963
|
+
}
|
964
|
+
|
965
|
+
analysis["total_execution_time"] += result.get("execution_time", 0)
|
966
|
+
|
967
|
+
# Generate responsive insights
|
968
|
+
if len(viewport_names) >= 2:
|
969
|
+
analysis["responsive_insights"].extend([
|
970
|
+
f"Tested across {len(viewport_names)} viewports: {', '.join(viewport_names)}",
|
971
|
+
f"Total execution time: {analysis['total_execution_time']:.2f}s",
|
972
|
+
f"Average time per viewport: {analysis['total_execution_time'] / len(viewport_names):.2f}s"
|
973
|
+
])
|
974
|
+
|
975
|
+
# Performance comparison
|
976
|
+
execution_times = [results[vp].get("execution_time", 0) for vp in viewport_names]
|
977
|
+
fastest_vp = viewport_names[execution_times.index(min(execution_times))]
|
978
|
+
slowest_vp = viewport_names[execution_times.index(max(execution_times))]
|
979
|
+
|
980
|
+
analysis["performance_analysis"] = {
|
981
|
+
"fastest_viewport": fastest_vp,
|
982
|
+
"slowest_viewport": slowest_vp,
|
983
|
+
"time_difference": max(execution_times) - min(execution_times),
|
984
|
+
"performance_variance": "low" if max(execution_times) - min(execution_times) < 2 else "high"
|
985
|
+
}
|
986
|
+
|
987
|
+
return analysis
|
988
|
+
|
989
|
+
def _collect_responsive_screenshots(self, results: Dict) -> List[Dict]:
|
990
|
+
"""Collect all screenshots from responsive testing"""
|
991
|
+
all_screenshots = []
|
992
|
+
for viewport_name, result in results.items():
|
993
|
+
if result.get("success") and "artifacts" in result:
|
994
|
+
for screenshot in result["artifacts"].get("screenshots", []):
|
995
|
+
screenshot["viewport"] = viewport_name
|
996
|
+
all_screenshots.append(screenshot)
|
997
|
+
return all_screenshots
|
998
|
+
|
999
|
+
def _collect_responsive_data(self, results: Dict) -> List[Dict]:
|
1000
|
+
"""Collect all comprehensive data from responsive testing"""
|
1001
|
+
all_data = []
|
1002
|
+
for viewport_name, result in results.items():
|
1003
|
+
if result.get("success") and "artifacts" in result:
|
1004
|
+
for data in result["artifacts"].get("comprehensive_data", []):
|
1005
|
+
data["viewport"] = viewport_name
|
1006
|
+
all_data.append(data)
|
1007
|
+
return all_data
|
1008
|
+
|
1009
|
+
def _recommend_best_iteration(self, iterations: List[Dict]) -> Optional[str]:
|
759
1010
|
best_iteration = None
|
760
1011
|
best_score = -1
|
761
1012
|
|
@@ -125,9 +125,9 @@ def create_config_template(project_path: Path, force: bool = False):
|
|
125
125
|
# Get current version
|
126
126
|
try:
|
127
127
|
import cursorflow
|
128
|
-
current_version = getattr(cursorflow, '__version__', '2.0
|
128
|
+
current_version = getattr(cursorflow, '__version__', '2.1.0')
|
129
129
|
except ImportError:
|
130
|
-
current_version = '2.0
|
130
|
+
current_version = '2.1.0'
|
131
131
|
|
132
132
|
if config_path.exists():
|
133
133
|
if not force:
|
@@ -302,9 +302,9 @@ def setup_update_checking(project_path: Path):
|
|
302
302
|
# Create initial version tracking
|
303
303
|
try:
|
304
304
|
import cursorflow
|
305
|
-
current_version = getattr(cursorflow, '__version__', '2.0
|
305
|
+
current_version = getattr(cursorflow, '__version__', '2.1.0')
|
306
306
|
except ImportError:
|
307
|
-
current_version = '2.0
|
307
|
+
current_version = '2.1.0'
|
308
308
|
|
309
309
|
version_info = {
|
310
310
|
"installed_version": current_version,
|