cursorflow 2.4.2__tar.gz → 2.5.0__tar.gz
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-2.4.2 → cursorflow-2.5.0}/PKG-INFO +8 -6
- {cursorflow-2.4.2 → cursorflow-2.5.0}/README.md +7 -5
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/cli.py +13 -22
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/browser_controller.py +5 -31
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/cursorflow.py +3 -13
- cursorflow-2.5.0/cursorflow/core/json_utils.py +53 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/mockup_comparator.py +35 -59
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/rules/cursorflow-usage.mdc +25 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow.egg-info/SOURCES.txt +1 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/docs/user/USAGE_GUIDE.md +198 -0
- cursorflow-2.5.0/examples/mockup_comparison_example.py +195 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/pyproject.toml +2 -2
- cursorflow-2.4.2/examples/mockup_comparison_example.py +0 -316
- {cursorflow-2.4.2 → cursorflow-2.5.0}/LICENSE +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/MANIFEST.in +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/__init__.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/auto_init.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/auto_updater.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/action_validator.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/agent.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/auth_handler.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/browser_engine.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/config_validator.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/css_iterator.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/cursor_integration.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/error_context_collector.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/error_correlator.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/event_correlator.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/file_change_monitor.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/hmr_detector.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/log_collector.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/log_monitor.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/persistent_session.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/report_generator.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/trace_manager.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/install_cursorflow_rules.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/log_sources/local_file.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/log_sources/ssh_remote.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/post_install.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/rules/__init__.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/rules/cursorflow-installation.mdc +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/updater.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/comprehensive_screenshot_example.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/element_inspection_example.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/element_measurement_example.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/enhanced_screenshot_example.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/hot_reload_css_iteration.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/opensas_example.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/react_example.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/responsive_testing_example.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/v2_comprehensive_demo.py +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/setup.cfg +0 -0
- {cursorflow-2.4.2 → cursorflow-2.5.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cursorflow
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.5.0
|
4
4
|
Summary: 🔥 Complete page intelligence for AI-driven development with Hot Reload Intelligence - captures DOM, network, console, performance, HMR events, and comprehensive page analysis
|
5
5
|
Author-email: GeekWarrior Development <rbush@cooltheory.com>
|
6
6
|
License-Expression: MIT
|
@@ -370,20 +370,22 @@ cursorflow test --base-url http://localhost:3000 --responsive --actions '[
|
|
370
370
|
cursorflow test --base-url http://localhost:3000 --path "/api" --output "api-test-results.json"
|
371
371
|
```
|
372
372
|
|
373
|
-
### **Design Comparison**
|
373
|
+
### **Design Comparison** (Pure Measurement)
|
374
374
|
```bash
|
375
|
-
# Compare mockup to implementation
|
375
|
+
# Compare mockup to implementation - get similarity metrics
|
376
376
|
cursorflow compare-mockup https://mockup.com/design \
|
377
377
|
--base-url http://localhost:3000 \
|
378
|
-
--mockup-actions '[{"navigate": "/"}]' \
|
379
378
|
--implementation-actions '[{"navigate": "/dashboard"}]'
|
379
|
+
# Output: 87.3% similarity, diff images, element measurements
|
380
380
|
|
381
|
-
# CSS
|
381
|
+
# Test CSS variations - observe real rendering
|
382
382
|
cursorflow iterate-mockup https://mockup.com/design \
|
383
383
|
--base-url http://localhost:5173 \
|
384
384
|
--css-improvements '[
|
385
|
-
{"name": "fix
|
385
|
+
{"name": "spacing-fix", "css": ".container { gap: 2rem; }"},
|
386
|
+
{"name": "tighter-spacing", "css": ".container { gap: 1rem; }"}
|
386
387
|
]'
|
388
|
+
# Output: Similarity data for each variation (Cursor decides which to apply)
|
387
389
|
```
|
388
390
|
|
389
391
|
### **Element Analysis & CSS Debugging**
|
@@ -325,20 +325,22 @@ cursorflow test --base-url http://localhost:3000 --responsive --actions '[
|
|
325
325
|
cursorflow test --base-url http://localhost:3000 --path "/api" --output "api-test-results.json"
|
326
326
|
```
|
327
327
|
|
328
|
-
### **Design Comparison**
|
328
|
+
### **Design Comparison** (Pure Measurement)
|
329
329
|
```bash
|
330
|
-
# Compare mockup to implementation
|
330
|
+
# Compare mockup to implementation - get similarity metrics
|
331
331
|
cursorflow compare-mockup https://mockup.com/design \
|
332
332
|
--base-url http://localhost:3000 \
|
333
|
-
--mockup-actions '[{"navigate": "/"}]' \
|
334
333
|
--implementation-actions '[{"navigate": "/dashboard"}]'
|
334
|
+
# Output: 87.3% similarity, diff images, element measurements
|
335
335
|
|
336
|
-
# CSS
|
336
|
+
# Test CSS variations - observe real rendering
|
337
337
|
cursorflow iterate-mockup https://mockup.com/design \
|
338
338
|
--base-url http://localhost:5173 \
|
339
339
|
--css-improvements '[
|
340
|
-
{"name": "fix
|
340
|
+
{"name": "spacing-fix", "css": ".container { gap: 2rem; }"},
|
341
|
+
{"name": "tighter-spacing", "css": ".container { gap: 1rem; }"}
|
341
342
|
]'
|
343
|
+
# Output: Similarity data for each variation (Cursor decides which to apply)
|
342
344
|
```
|
343
345
|
|
344
346
|
### **Element Analysis & CSS Debugging**
|
@@ -357,13 +357,11 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
|
|
357
357
|
help='JSON array of viewports to test: [{"width": 1440, "height": 900, "name": "desktop"}]')
|
358
358
|
@click.option('--diff-threshold', '-t', type=float, default=0.1,
|
359
359
|
help='Visual difference threshold (0.0-1.0)')
|
360
|
-
@click.option('--block-tracking', is_flag=True,
|
361
|
-
help='Block tracking scripts (Google Analytics, Facebook Pixel, etc.) to prevent timeout')
|
362
360
|
@click.option('--output', '-o', default='mockup_comparison_results.json',
|
363
361
|
help='Output file for comparison results')
|
364
362
|
@click.option('--verbose', is_flag=True,
|
365
363
|
help='Verbose output')
|
366
|
-
def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions, viewports, diff_threshold,
|
364
|
+
def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions, viewports, diff_threshold, output, verbose):
|
367
365
|
"""Compare mockup design to work-in-progress implementation"""
|
368
366
|
|
369
367
|
console.print(f"🎨 Comparing mockup [blue]{mockup_url}[/blue] to implementation [blue]{base_url}[/blue]")
|
@@ -407,16 +405,10 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
|
|
407
405
|
try:
|
408
406
|
from .core.cursorflow import CursorFlow
|
409
407
|
|
410
|
-
# Configure browser with tracking blocking if requested
|
411
|
-
browser_config = {'headless': True}
|
412
|
-
if block_tracking:
|
413
|
-
browser_config['block_tracking'] = True
|
414
|
-
console.print("🚫 Blocking tracking scripts (Google Analytics, Facebook Pixel, etc.)")
|
415
|
-
|
416
408
|
flow = CursorFlow(
|
417
409
|
base_url=base_url,
|
418
410
|
log_config={'source': 'local', 'paths': ['logs/app.log']},
|
419
|
-
browser_config=
|
411
|
+
browser_config={'headless': True}
|
420
412
|
)
|
421
413
|
except Exception as e:
|
422
414
|
console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
|
@@ -436,22 +428,20 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
|
|
436
428
|
console.print(f"[red]❌ Comparison failed: {results['error']}[/red]")
|
437
429
|
return
|
438
430
|
|
439
|
-
# Display results summary
|
431
|
+
# Display results summary (pure metrics only)
|
440
432
|
summary = results.get('summary', {})
|
441
433
|
console.print(f"✅ Comparison completed: {results.get('comparison_id', 'unknown')}")
|
442
434
|
console.print(f"📊 Average similarity: [bold]{summary.get('average_similarity', 0)}%[/bold]")
|
443
435
|
console.print(f"📱 Viewports tested: {summary.get('viewports_tested', 0)}")
|
444
436
|
|
445
|
-
# Show
|
446
|
-
|
447
|
-
if
|
448
|
-
console.print(f"
|
449
|
-
for i, rec in enumerate(recommendations[:3]): # Show first 3
|
450
|
-
console.print(f" {i+1}. {rec.get('description', 'No description')}")
|
437
|
+
# Show similarity range
|
438
|
+
similarity_range = summary.get('similarity_range', {})
|
439
|
+
if similarity_range:
|
440
|
+
console.print(f"📈 Similarity range: {similarity_range.get('min', 0)}% - {similarity_range.get('max', 0)}%")
|
451
441
|
|
452
|
-
# Save results
|
453
|
-
|
454
|
-
|
442
|
+
# Save results with safe serialization
|
443
|
+
from .core.json_utils import safe_json_dump
|
444
|
+
safe_json_dump(results, output)
|
455
445
|
|
456
446
|
console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
|
457
447
|
console.print(f"📁 Visual diffs stored in: [cyan].cursorflow/artifacts/[/cyan]")
|
@@ -552,9 +542,10 @@ def iterate_mockup(mockup_url, base_url, css_improvements, base_actions, diff_th
|
|
552
542
|
for i, rec in enumerate(recommendations[:3]):
|
553
543
|
console.print(f" {i+1}. {rec.get('description', 'No description')}")
|
554
544
|
|
555
|
-
# Save results
|
545
|
+
# Save results with numpy type handling
|
546
|
+
from cursorflow.core.json_utils import safe_json_serialize
|
556
547
|
with open(output, 'w') as f:
|
557
|
-
json.dump(results, f, indent=2, default=
|
548
|
+
json.dump(results, f, indent=2, default=safe_json_serialize)
|
558
549
|
|
559
550
|
console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
|
560
551
|
console.print(f"📁 Iteration progress stored in: [cyan].cursorflow/artifacts/[/cyan]")
|
@@ -131,9 +131,9 @@ class BrowserController:
|
|
131
131
|
self.context = await self.browser.new_context(**context_config)
|
132
132
|
self.page = await self.context.new_page()
|
133
133
|
|
134
|
-
#
|
135
|
-
|
136
|
-
|
134
|
+
# Note: We do NOT block tracking scripts by default
|
135
|
+
# CursorFlow philosophy: "Capture reality, not fiction"
|
136
|
+
# Blocking scripts would alter the actual page behavior we're measuring
|
137
137
|
|
138
138
|
# v2.0 Enhancement: Initialize Error Context Collector
|
139
139
|
self.error_context_collector = ErrorContextCollector(self.page, self.logger)
|
@@ -173,33 +173,6 @@ class BrowserController:
|
|
173
173
|
|
174
174
|
raise
|
175
175
|
|
176
|
-
async def _setup_tracking_blocker(self):
|
177
|
-
"""Block common tracking scripts to prevent networkidle timeout"""
|
178
|
-
tracking_patterns = [
|
179
|
-
"*google-analytics.com/*",
|
180
|
-
"*googletagmanager.com/*",
|
181
|
-
"*facebook.com/tr/*",
|
182
|
-
"*facebook.net/*",
|
183
|
-
"*doubleclick.net/*",
|
184
|
-
"*analytics.google.com/*",
|
185
|
-
"*hotjar.com/*",
|
186
|
-
"*mixpanel.com/*",
|
187
|
-
"*segment.com/*",
|
188
|
-
"*googleadservices.com/*",
|
189
|
-
"*connect.facebook.net/*",
|
190
|
-
"*/analytics/*",
|
191
|
-
"*/tracking/*"
|
192
|
-
]
|
193
|
-
|
194
|
-
async def block_tracking(route):
|
195
|
-
"""Block tracking request"""
|
196
|
-
await route.abort()
|
197
|
-
|
198
|
-
for pattern in tracking_patterns:
|
199
|
-
await self.page.route(pattern, block_tracking)
|
200
|
-
|
201
|
-
self.logger.info("🚫 Tracking scripts blocked")
|
202
|
-
|
203
176
|
async def _setup_event_listeners(self):
|
204
177
|
"""Set up universal event listeners for any framework"""
|
205
178
|
|
@@ -388,8 +361,9 @@ class BrowserController:
|
|
388
361
|
self.logger.info(f"Navigating to: {url}")
|
389
362
|
|
390
363
|
# Navigate and wait
|
364
|
+
# Use 'load' instead of 'networkidle' to avoid timeout on pages with tracking scripts
|
391
365
|
if wait_for_load:
|
392
|
-
await self.page.goto(url, wait_until="
|
366
|
+
await self.page.goto(url, wait_until="load", timeout=30000)
|
393
367
|
else:
|
394
368
|
await self.page.goto(url, timeout=30000)
|
395
369
|
|
@@ -575,19 +575,9 @@ class CursorFlow:
|
|
575
575
|
if "error" in raw_results:
|
576
576
|
return raw_results
|
577
577
|
|
578
|
-
#
|
579
|
-
|
580
|
-
|
581
|
-
raw_results=raw_results,
|
582
|
-
session_id=session_id,
|
583
|
-
project_context={
|
584
|
-
"framework": "auto-detected",
|
585
|
-
"base_url": self.base_url,
|
586
|
-
"test_type": "mockup_comparison"
|
587
|
-
}
|
588
|
-
)
|
589
|
-
|
590
|
-
return cursor_results
|
578
|
+
# Return raw measurement data directly (pure observation)
|
579
|
+
# No interpretation, no analysis - Cursor makes decisions based on data
|
580
|
+
return raw_results
|
591
581
|
|
592
582
|
except Exception as e:
|
593
583
|
self.logger.error(f"Mockup comparison failed: {e}")
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"""
|
2
|
+
JSON Serialization Utilities
|
3
|
+
|
4
|
+
Handles conversion of numpy and other non-standard types to JSON-safe Python types.
|
5
|
+
Essential for mockup comparison and any feature using PIL/numpy for image analysis.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
from typing import Any
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
|
13
|
+
def safe_json_serialize(obj: Any) -> Any:
|
14
|
+
"""
|
15
|
+
Convert any Python/numpy type to JSON-safe type
|
16
|
+
|
17
|
+
Handles:
|
18
|
+
- numpy scalars (bool_, int_, float_)
|
19
|
+
- numpy arrays (ndarray)
|
20
|
+
- Other non-serializable types (converts to string)
|
21
|
+
|
22
|
+
Args:
|
23
|
+
obj: Any object that might not be JSON serializable
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
JSON-safe Python type
|
27
|
+
"""
|
28
|
+
# numpy scalars (bool_, int64, float64, etc.)
|
29
|
+
if hasattr(obj, 'item'):
|
30
|
+
return obj.item()
|
31
|
+
|
32
|
+
# numpy arrays
|
33
|
+
if hasattr(obj, 'tolist'):
|
34
|
+
return obj.tolist()
|
35
|
+
|
36
|
+
# Fallback for anything else
|
37
|
+
return str(obj)
|
38
|
+
|
39
|
+
|
40
|
+
def safe_json_dump(data: Any, file_path: str, indent: int = 2) -> None:
|
41
|
+
"""
|
42
|
+
Safely dump data to JSON file with numpy type handling
|
43
|
+
|
44
|
+
Args:
|
45
|
+
data: Data to serialize
|
46
|
+
file_path: Path to output file
|
47
|
+
indent: JSON indentation level
|
48
|
+
"""
|
49
|
+
file_path = Path(file_path)
|
50
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
51
|
+
|
52
|
+
with open(file_path, 'w') as f:
|
53
|
+
json.dump(data, f, indent=indent, default=safe_json_serialize)
|
@@ -11,6 +11,8 @@ import json
|
|
11
11
|
from typing import Dict, List, Optional, Any, Tuple
|
12
12
|
from pathlib import Path
|
13
13
|
import logging
|
14
|
+
|
15
|
+
from .json_utils import safe_json_dump, safe_json_serialize
|
14
16
|
try:
|
15
17
|
from PIL import Image, ImageDraw, ImageChops
|
16
18
|
import numpy as np
|
@@ -107,9 +109,14 @@ class MockupComparator:
|
|
107
109
|
await mockup_browser.set_viewport(viewport["width"], viewport["height"])
|
108
110
|
await implementation_browser.set_viewport(viewport["width"], viewport["height"])
|
109
111
|
|
110
|
-
# Navigate to initial pages
|
112
|
+
# Navigate to initial pages with stabilization
|
111
113
|
await mockup_browser.navigate("/")
|
114
|
+
await asyncio.sleep(1)
|
115
|
+
await mockup_browser.page.wait_for_load_state("domcontentloaded")
|
116
|
+
|
112
117
|
await implementation_browser.navigate("/")
|
118
|
+
await asyncio.sleep(1)
|
119
|
+
await implementation_browser.page.wait_for_load_state("domcontentloaded")
|
113
120
|
|
114
121
|
# Execute any required actions on mockup
|
115
122
|
if mockup_actions:
|
@@ -119,6 +126,9 @@ class MockupComparator:
|
|
119
126
|
if implementation_actions:
|
120
127
|
await self._execute_actions_on_browser(implementation_browser, implementation_actions)
|
121
128
|
|
129
|
+
# Final stabilization before capturing
|
130
|
+
await asyncio.sleep(0.5)
|
131
|
+
|
122
132
|
# Capture screenshots
|
123
133
|
mockup_screenshot = await self._capture_comparison_screenshot(
|
124
134
|
mockup_browser, f"{comparison_name}_mockup_{viewport_name}"
|
@@ -163,14 +173,12 @@ class MockupComparator:
|
|
163
173
|
"implementation_url": implementation_url,
|
164
174
|
"viewports_tested": len(viewports),
|
165
175
|
"results": comparison_results,
|
166
|
-
"summary": self._create_comparison_summary(comparison_results)
|
167
|
-
"recommendations": self._generate_improvement_recommendations(comparison_results)
|
176
|
+
"summary": self._create_comparison_summary(comparison_results)
|
168
177
|
}
|
169
178
|
|
170
|
-
# Save comparison report
|
179
|
+
# Save comparison report with safe serialization
|
171
180
|
report_path = self.artifacts_base / "mockup_comparisons" / f"{comparison_name}.json"
|
172
|
-
|
173
|
-
json.dump(comparison_report, f, indent=2, default=str)
|
181
|
+
safe_json_dump(comparison_report, str(report_path))
|
174
182
|
|
175
183
|
self.logger.info(f"Mockup comparison completed: {comparison_name}")
|
176
184
|
return comparison_report
|
@@ -288,10 +296,9 @@ class MockupComparator:
|
|
288
296
|
"final_recommendations": self._generate_final_recommendations(iteration_results)
|
289
297
|
}
|
290
298
|
|
291
|
-
# Save iteration report
|
299
|
+
# Save iteration report with safe JSON serialization
|
292
300
|
report_path = self.artifacts_base / "iteration_progress" / f"{iteration_session_id}.json"
|
293
|
-
|
294
|
-
json.dump(iteration_report, f, indent=2, default=str)
|
301
|
+
safe_json_dump(iteration_report, str(report_path))
|
295
302
|
|
296
303
|
self.logger.info(f"UI matching iteration completed: {iteration_session_id}")
|
297
304
|
return iteration_report
|
@@ -309,11 +316,15 @@ class MockupComparator:
|
|
309
316
|
if "navigate" in action:
|
310
317
|
path = action["navigate"]
|
311
318
|
await browser.navigate(path)
|
319
|
+
# Wait for page to stabilize after navigation
|
320
|
+
await asyncio.sleep(1) # Brief pause for dynamic content rendering
|
321
|
+
await browser.page.wait_for_load_state("domcontentloaded")
|
312
322
|
elif "click" in action:
|
313
323
|
selector = action["click"]
|
314
324
|
if isinstance(selector, dict):
|
315
325
|
selector = selector["selector"]
|
316
326
|
await browser.click(selector)
|
327
|
+
await asyncio.sleep(0.5) # Brief pause for UI response
|
317
328
|
elif "wait_for" in action:
|
318
329
|
selector = action["wait_for"]
|
319
330
|
if isinstance(selector, dict):
|
@@ -324,6 +335,7 @@ class MockupComparator:
|
|
324
335
|
elif "scroll" in action:
|
325
336
|
scroll_config = action["scroll"]
|
326
337
|
await browser.page.evaluate(f"window.scrollTo({scroll_config.get('x', 0)}, {scroll_config.get('y', 0)})")
|
338
|
+
await asyncio.sleep(0.3) # Brief pause for scroll rendering
|
327
339
|
|
328
340
|
async def _capture_comparison_screenshot(self, browser: BrowserController, name: str) -> str:
|
329
341
|
"""Capture screenshot for comparison"""
|
@@ -1194,11 +1206,11 @@ class MockupComparator:
|
|
1194
1206
|
improvement = improved_similarity - baseline_similarity
|
1195
1207
|
|
1196
1208
|
return {
|
1197
|
-
"baseline_similarity": baseline_similarity,
|
1198
|
-
"improved_similarity": improved_similarity,
|
1199
|
-
"improvement": improvement,
|
1200
|
-
"improvement_percentage": round(improvement, 2),
|
1201
|
-
"is_improvement": improvement > 0
|
1209
|
+
"baseline_similarity": float(baseline_similarity),
|
1210
|
+
"improved_similarity": float(improved_similarity),
|
1211
|
+
"improvement": float(improvement),
|
1212
|
+
"improvement_percentage": round(float(improvement), 2),
|
1213
|
+
"is_improvement": bool(improvement > 0) # Convert to Python bool
|
1202
1214
|
}
|
1203
1215
|
|
1204
1216
|
except Exception as e:
|
@@ -1218,55 +1230,19 @@ class MockupComparator:
|
|
1218
1230
|
|
1219
1231
|
avg_similarity = sum(similarities) / len(similarities) if similarities else 0
|
1220
1232
|
|
1233
|
+
# Pure data summary - no interpretation
|
1221
1234
|
return {
|
1222
|
-
"average_similarity": round(avg_similarity, 2),
|
1235
|
+
"average_similarity": round(float(avg_similarity), 2),
|
1223
1236
|
"viewports_tested": len(comparison_results),
|
1224
|
-
"
|
1225
|
-
|
1237
|
+
"similarity_by_viewport": [
|
1238
|
+
{
|
1239
|
+
"viewport": result.get("viewport", {}).get("name", "unknown"),
|
1240
|
+
"similarity": float(result.get("visual_diff", {}).get("similarity_score", 0))
|
1241
|
+
}
|
1242
|
+
for result in comparison_results
|
1243
|
+
]
|
1226
1244
|
}
|
1227
1245
|
|
1228
|
-
def _generate_improvement_recommendations(self, comparison_results: List[Dict]) -> List[Dict]:
|
1229
|
-
"""Generate recommendations for improving implementation to match mockup"""
|
1230
|
-
recommendations = []
|
1231
|
-
|
1232
|
-
for result in comparison_results:
|
1233
|
-
viewport = result.get("viewport", {})
|
1234
|
-
layout_analysis = result.get("layout_analysis", {})
|
1235
|
-
visual_diff = result.get("visual_diff", {})
|
1236
|
-
|
1237
|
-
# Analyze layout differences for recommendations
|
1238
|
-
differences = layout_analysis.get("differences", [])
|
1239
|
-
|
1240
|
-
for diff in differences:
|
1241
|
-
if diff["type"] == "missing_in_implementation":
|
1242
|
-
recommendations.append({
|
1243
|
-
"type": "add_element",
|
1244
|
-
"priority": "high",
|
1245
|
-
"description": f"Add missing element: {diff['selector']}",
|
1246
|
-
"viewport": viewport.get("name", "unknown")
|
1247
|
-
})
|
1248
|
-
elif diff["type"] == "property_differences":
|
1249
|
-
for prop, prop_diff in diff.get("differences", {}).items():
|
1250
|
-
recommendations.append({
|
1251
|
-
"type": "adjust_property",
|
1252
|
-
"priority": "medium",
|
1253
|
-
"description": f"Adjust {prop} for {diff['selector']}",
|
1254
|
-
"current_value": prop_diff.get("implementation"),
|
1255
|
-
"target_value": prop_diff.get("mockup"),
|
1256
|
-
"viewport": viewport.get("name", "unknown")
|
1257
|
-
})
|
1258
|
-
|
1259
|
-
# Add visual similarity recommendations
|
1260
|
-
similarity = visual_diff.get("similarity_score", 0)
|
1261
|
-
if similarity < 70:
|
1262
|
-
recommendations.append({
|
1263
|
-
"type": "major_visual_changes",
|
1264
|
-
"priority": "high",
|
1265
|
-
"description": f"Significant visual differences detected (similarity: {similarity}%)",
|
1266
|
-
"viewport": viewport.get("name", "unknown")
|
1267
|
-
})
|
1268
|
-
|
1269
|
-
return recommendations
|
1270
1246
|
|
1271
1247
|
def _create_iteration_summary(self, baseline_comparison: Dict, iteration_results: List[Dict]) -> Dict[str, Any]:
|
1272
1248
|
"""Create summary of iteration session"""
|
@@ -425,6 +425,31 @@ cursorflow measure -u http://localhost:3000 -s "#panel"
|
|
425
425
|
# Output: 532w × 900h ✅
|
426
426
|
```
|
427
427
|
|
428
|
+
### Visual Comparison & Iteration
|
429
|
+
```bash
|
430
|
+
# Compare mockup to implementation (pure measurement)
|
431
|
+
cursorflow compare-mockup MOCKUP_URL -u BASE_URL -ia '[{"navigate": "/page"}]'
|
432
|
+
# Output: Similarity percentage, diff images, element data
|
433
|
+
|
434
|
+
# Test CSS variations (observe real rendering)
|
435
|
+
cursorflow iterate-mockup MOCKUP_URL -u BASE_URL --css-improvements '[
|
436
|
+
{"name": "test1", "css": ".header { padding: 2rem; }"},
|
437
|
+
{"name": "test2", "css": ".header { padding: 1rem; }"}
|
438
|
+
]'
|
439
|
+
# Output: Similarity for each variation - Cursor decides which to apply
|
440
|
+
```
|
441
|
+
|
442
|
+
**Philosophy**:
|
443
|
+
- CursorFlow observes mockup and implementation (both are reality)
|
444
|
+
- Provides quantified measurements (similarity %, diff images, element data)
|
445
|
+
- Temporarily injects CSS to observe what reality WOULD look like
|
446
|
+
- Cursor analyzes the data and makes decisions
|
447
|
+
|
448
|
+
**When to use**:
|
449
|
+
- User has design mockup and needs to match it
|
450
|
+
- Testing multiple CSS approaches before applying
|
451
|
+
- Measuring progress toward design specifications
|
452
|
+
|
428
453
|
## Analyzing Results
|
429
454
|
|
430
455
|
### **Hot Reload CSS Iteration Results**
|
@@ -24,6 +24,7 @@ cursorflow/core/error_correlator.py
|
|
24
24
|
cursorflow/core/event_correlator.py
|
25
25
|
cursorflow/core/file_change_monitor.py
|
26
26
|
cursorflow/core/hmr_detector.py
|
27
|
+
cursorflow/core/json_utils.py
|
27
28
|
cursorflow/core/log_collector.py
|
28
29
|
cursorflow/core/log_monitor.py
|
29
30
|
cursorflow/core/mockup_comparator.py
|