Spaces:
Sleeping
Sleeping
| """ | |
| Enhanced Report Generator | |
| Generates comprehensive reports with framework mapping and detailed analysis | |
| """ | |
| import os | |
| import json | |
| from datetime import datetime | |
| from typing import Dict, List, Any, Optional | |
| from pathlib import Path | |
| from state_schema import WorkflowState, VisualDifference | |
| class EnhancedReportGenerator: | |
| """Generate comprehensive regression testing reports""" | |
| # Framework categories | |
| FRAMEWORK_CATEGORIES = { | |
| "Layout & Structure": 8, | |
| "Typography": 10, | |
| "Colors & Contrast": 10, | |
| "Spacing & Sizing": 8, | |
| "Borders & Outlines": 6, | |
| "Shadows & Effects": 7, | |
| "Components & Elements": 10, | |
| "Buttons & Interactive": 10, | |
| "Forms & Inputs": 10, | |
| "Images & Media": 8 | |
| } | |
| def __init__(self, state: Dict[str, Any], output_dir: str): | |
| """Initialize report generator""" | |
| self.state = state | |
| self.output_dir = output_dir | |
| os.makedirs(output_dir, exist_ok=True) | |
| def generate_all_reports(self): | |
| """Generate all report types""" | |
| self.generate_summary_report() | |
| self.generate_detailed_report() | |
| self.generate_framework_mapping_report() | |
| self.generate_json_report() | |
| self.generate_html_report() | |
| def generate_summary_report(self) -> str: | |
| """Generate summary markdown report""" | |
| lines = [] | |
| lines.append("# π¨ UI Regression Testing Report - Summary\n") | |
| lines.append(f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") | |
| lines.append(f"**Execution ID**: {self.state.get('execution_id', 'unknown')}\n") | |
| lines.append(f"**Website**: {self.state.get('website_url', 'unknown')}\n") | |
| lines.append(f"**Figma File**: {self.state.get('figma_file_key', 'unknown')}\n\n") | |
| # Overall Score | |
| lines.append("## π Overall Results\n") | |
| lines.append(f"- **Similarity Score**: {self.state.get('similarity_score', 0.0):.1f}/100") | |
| # Severity breakdown | |
| visual_differences = self.state.get('visual_differences', []) | |
| high = len([d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == "High"]) | |
| medium = len([d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == "Medium"]) | |
| low = len([d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == "Low"]) | |
| total = len(visual_differences) | |
| lines.append(f"- **Total Differences**: {total}") | |
| lines.append(f"- π΄ **High Severity**: {high}") | |
| lines.append(f"- π **Medium Severity**: {medium}") | |
| lines.append(f"- π’ **Low Severity**: {low}\n") | |
| # Category breakdown | |
| categories = self._group_by_category() | |
| if categories: | |
| lines.append("## π Issues by Category\n") | |
| for category in sorted(categories.keys()): | |
| count = len(categories[category]) | |
| high_count = len([d for d in categories[category] if d.severity == "High"]) | |
| lines.append(f"- **{category}**: {count} issues ({high_count} high severity)") | |
| lines.append("") | |
| # Viewport breakdown | |
| lines.append("## π± Issues by Viewport\n") | |
| visual_differences = self.state.get('visual_differences', []) | |
| desktop_diffs = [d for d in visual_differences if (d.get('viewport') if isinstance(d, dict) else d.viewport) == "desktop"] | |
| mobile_diffs = [d for d in visual_differences if (d.get('viewport') if isinstance(d, dict) else d.viewport) == "mobile"] | |
| lines.append(f"- **Desktop (1440px)**: {len(desktop_diffs)} issues") | |
| lines.append(f"- **Mobile (375px)**: {len(mobile_diffs)} issues\n") | |
| # Recommendations | |
| lines.append("## π‘ Recommendations\n") | |
| if high > 0: | |
| lines.append(f"- π΄ **Critical**: Address all {high} high-severity issues immediately") | |
| if medium > 0: | |
| lines.append(f"- π **Important**: Schedule fixes for {medium} medium-severity issues") | |
| if low > 0: | |
| lines.append(f"- π’ **Nice to have**: Consider fixing {low} low-severity issues") | |
| report_path = os.path.join(self.output_dir, "report_summary.md") | |
| with open(report_path, 'w') as f: | |
| f.write("\n".join(lines)) | |
| return "\n".join(lines) | |
| def generate_detailed_report(self) -> str: | |
| """Generate detailed markdown report""" | |
| lines = [] | |
| lines.append("# π UI Regression Testing Report - Detailed\n") | |
| lines.append(f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") | |
| # Executive Summary | |
| lines.append("## Executive Summary\n") | |
| lines.append(f"This report details all visual differences detected between the Figma design ") | |
| lines.append(f"and the live website. The analysis was performed on {len(self.state.get('viewports', []))} viewports ") | |
| lines.append(f"using a hybrid approach combining screenshot analysis, CSS extraction, and AI vision models.\n\n") | |
| # Test Configuration | |
| lines.append("## Test Configuration\n") | |
| lines.append(f"- **Website URL**: {self.state.get('website_url', 'unknown')}") | |
| lines.append(f"- **Figma File**: {self.state.get('figma_file_key', 'unknown')}") | |
| lines.append(f"- **Execution ID**: {self.state.get('execution_id', 'unknown')}") | |
| lines.append(f"- **Viewports Tested**: Desktop (1440px), Mobile (375px)\n\n") | |
| # Results Overview | |
| lines.append("## Results Overview\n") | |
| visual_differences = self.state.get('visual_differences', []) | |
| lines.append(f"- **Similarity Score**: {self.state.get('similarity_score', 0.0):.1f}/100") | |
| lines.append(f"- **Total Differences Found**: {len(visual_differences)}") | |
| lines.append(f"- **High Severity**: {len([d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == 'High'])}") | |
| lines.append(f"- **Medium Severity**: {len([d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == 'Medium'])}") | |
| lines.append(f"- **Low Severity**: {len([d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == 'Low'])}\n\n") | |
| # Detailed Findings | |
| lines.append("## Detailed Findings\n\n") | |
| # Group by severity | |
| visual_differences = self.state.get('visual_differences', []) | |
| high_diffs = [d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == "High"] | |
| medium_diffs = [d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == "Medium"] | |
| low_diffs = [d for d in visual_differences if (d.get('severity') if isinstance(d, dict) else d.severity) == "Low"] | |
| if high_diffs: | |
| lines.append("### π΄ High Severity Issues\n") | |
| for i, diff in enumerate(high_diffs, 1): | |
| name = diff.get('title') if isinstance(diff, dict) else getattr(diff, 'title', 'Difference') | |
| category = diff.get('category') if isinstance(diff, dict) else getattr(diff, 'category', 'visual') | |
| description = diff.get('description') if isinstance(diff, dict) else getattr(diff, 'description', '') | |
| viewport = diff.get('viewport') if isinstance(diff, dict) else getattr(diff, 'viewport', 'desktop') | |
| detection_method = diff.get('detection_method') if isinstance(diff, dict) else getattr(diff, 'detection_method', 'manual') | |
| confidence = diff.get('confidence', 1.0) if isinstance(diff, dict) else getattr(diff, 'confidence', 1.0) | |
| lines.append(f"#### {i}. {name}\n") | |
| lines.append(f"- **Category**: {category}") | |
| lines.append(f"- **Description**: {description}") | |
| lines.append(f"- **Viewport**: {viewport}") | |
| lines.append(f"- **Detection Method**: {detection_method}") | |
| lines.append(f"- **Confidence**: {confidence*100:.0f}%\n") | |
| if medium_diffs: | |
| lines.append("### π Medium Severity Issues\n") | |
| for i, diff in enumerate(medium_diffs, 1): | |
| lines.append(f"#### {i}. {diff.name}\n") | |
| lines.append(f"- **Category**: {diff.category}") | |
| lines.append(f"- **Description**: {diff.description}") | |
| lines.append(f"- **Viewport**: {diff.viewport}") | |
| lines.append(f"- **Detection Method**: {diff.detection_method}") | |
| lines.append(f"- **Confidence**: {diff.confidence*100:.0f}%\n") | |
| if low_diffs: | |
| lines.append("### π’ Low Severity Issues\n") | |
| for i, diff in enumerate(low_diffs, 1): | |
| lines.append(f"#### {i}. {diff.name}\n") | |
| lines.append(f"- **Category**: {diff.category}") | |
| lines.append(f"- **Description**: {diff.description}") | |
| lines.append(f"- **Viewport**: {diff.viewport}") | |
| lines.append(f"- **Detection Method**: {diff.detection_method}") | |
| lines.append(f"- **Confidence**: {diff.confidence*100:.0f}%\n") | |
| # Recommendations | |
| lines.append("## Recommendations\n\n") | |
| lines.append("### Immediate Actions (High Severity)\n") | |
| lines.append("1. Review all high-severity issues with the development team") | |
| lines.append("2. Create tickets for each issue in your project management system") | |
| lines.append("3. Prioritize fixes based on user impact\n\n") | |
| lines.append("### Short-term Actions (Medium Severity)\n") | |
| lines.append("1. Schedule review of medium-severity issues") | |
| lines.append("2. Determine if issues affect user experience") | |
| lines.append("3. Plan fixes in upcoming sprints\n\n") | |
| lines.append("### Continuous Improvement\n") | |
| lines.append("1. Run regression tests regularly (weekly/bi-weekly)") | |
| lines.append("2. Update Figma designs to match implementation") | |
| lines.append("3. Establish design-to-development handoff process\n") | |
| report_path = os.path.join(self.output_dir, "report_detailed.md") | |
| with open(report_path, 'w') as f: | |
| f.write("\n".join(lines)) | |
| return "\n".join(lines) | |
| def generate_framework_mapping_report(self) -> str: | |
| """Generate framework mapping report""" | |
| lines = [] | |
| lines.append("# π Framework Mapping Report\n") | |
| lines.append("## 114-Point Visual Differences Framework\n\n") | |
| lines.append("This report maps detected differences to the comprehensive 114-point framework ") | |
| lines.append("across 10 categories of visual properties.\n\n") | |
| # Framework summary | |
| lines.append("## Framework Coverage\n\n") | |
| lines.append("| Category | Total Issues | Detected | Coverage |\n") | |
| lines.append("|----------|-------------|----------|----------|\n") | |
| total_framework = 0 | |
| total_detected = 0 | |
| visual_differences = self.state.get('visual_differences', []) | |
| for category, total in self.FRAMEWORK_CATEGORIES.items(): | |
| detected = len([d for d in visual_differences if (d.get('category') if isinstance(d, dict) else d.category) == category]) | |
| coverage = (detected / total * 100) if total > 0 else 0 | |
| lines.append(f"| {category} | {total} | {detected} | {coverage:.0f}% |\n") | |
| total_framework += total | |
| total_detected += detected | |
| coverage = (total_detected / total_framework * 100) if total_framework > 0 else 0 | |
| lines.append(f"| **TOTAL** | **{total_framework}** | **{total_detected}** | **{coverage:.0f}%** |\n\n") | |
| # Detected issues by category | |
| lines.append("## Detected Issues by Category\n\n") | |
| categories = self._group_by_category() | |
| for category in sorted(categories.keys()): | |
| diffs = categories[category] | |
| lines.append(f"### {category} ({len(diffs)} issues)\n\n") | |
| for diff in diffs: | |
| lines.append(f"- **{diff.name}** ({diff.severity})") | |
| lines.append(f" - {diff.description}\n") | |
| report_path = os.path.join(self.output_dir, "report_framework.md") | |
| with open(report_path, 'w') as f: | |
| f.write("\n".join(lines)) | |
| return "\n".join(lines) | |
| def generate_json_report(self) -> str: | |
| """Generate JSON report for programmatic access""" | |
| report = { | |
| "metadata": { | |
| "generated": datetime.now().isoformat(), | |
| "execution_id": self.state.execution_id, | |
| "website_url": self.state.website_url, | |
| "figma_file": self.state.figma_file_key | |
| }, | |
| "summary": { | |
| "similarity_score": self.state.similarity_score, | |
| "total_differences": len(self.state.visual_differences), | |
| "high_severity": len([d for d in self.state.visual_differences if d.severity == "High"]), | |
| "medium_severity": len([d for d in self.state.visual_differences if d.severity == "Medium"]), | |
| "low_severity": len([d for d in self.state.visual_differences if d.severity == "Low"]) | |
| }, | |
| "differences": [ | |
| { | |
| "name": d.name, | |
| "category": d.category, | |
| "severity": d.severity, | |
| "description": d.description, | |
| "viewport": d.viewport, | |
| "detection_method": d.detection_method, | |
| "confidence": d.confidence, | |
| "location": d.location | |
| } | |
| for d in self.state.visual_differences | |
| ], | |
| "framework_coverage": { | |
| category: { | |
| "total": total, | |
| "detected": len([d for d in self.state.visual_differences if d.category == category]) | |
| } | |
| for category, total in self.FRAMEWORK_CATEGORIES.items() | |
| } | |
| } | |
| report_path = os.path.join(self.output_dir, "report.json") | |
| with open(report_path, 'w') as f: | |
| json.dump(report, f, indent=2) | |
| return json.dumps(report, indent=2) | |
| def generate_html_report(self) -> str: | |
| """Generate HTML report""" | |
| html_lines = [] | |
| html_lines.append("<!DOCTYPE html>") | |
| html_lines.append("<html>") | |
| html_lines.append("<head>") | |
| html_lines.append("<meta charset='UTF-8'>") | |
| html_lines.append("<meta name='viewport' content='width=device-width, initial-scale=1.0'>") | |
| html_lines.append("<title>UI Regression Testing Report</title>") | |
| html_lines.append("<style>") | |
| html_lines.append(self._get_html_styles()) | |
| html_lines.append("</style>") | |
| html_lines.append("</head>") | |
| html_lines.append("<body>") | |
| # Header | |
| html_lines.append("<div class='header'>") | |
| html_lines.append("<h1>π¨ UI Regression Testing Report</h1>") | |
| html_lines.append(f"<p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>") | |
| html_lines.append("</div>") | |
| # Summary | |
| html_lines.append("<div class='summary'>") | |
| html_lines.append("<h2>π Summary</h2>") | |
| html_lines.append(f"<p><strong>Similarity Score:</strong> {self.state.similarity_score:.1f}/100</p>") | |
| html_lines.append(f"<p><strong>Total Differences:</strong> {len(self.state.visual_differences)}</p>") | |
| html_lines.append("</div>") | |
| # Severity breakdown | |
| html_lines.append("<div class='severity'>") | |
| html_lines.append("<h3>Severity Breakdown</h3>") | |
| high = len([d for d in self.state.visual_differences if d.severity == "High"]) | |
| medium = len([d for d in self.state.visual_differences if d.severity == "Medium"]) | |
| low = len([d for d in self.state.visual_differences if d.severity == "Low"]) | |
| html_lines.append(f"<p>π΄ High: {high} | π Medium: {medium} | π’ Low: {low}</p>") | |
| html_lines.append("</div>") | |
| # Differences list | |
| html_lines.append("<div class='differences'>") | |
| html_lines.append("<h2>π Detected Differences</h2>") | |
| for diff in sorted(self.state.visual_differences, key=lambda d: d.severity, reverse=True): | |
| severity_emoji = "π΄" if diff.severity == "High" else "π " if diff.severity == "Medium" else "π’" | |
| html_lines.append(f"<div class='difference'>") | |
| html_lines.append(f"<h3>{severity_emoji} {diff.name}</h3>") | |
| html_lines.append(f"<p><strong>Category:</strong> {diff.category}</p>") | |
| html_lines.append(f"<p><strong>Description:</strong> {diff.description}</p>") | |
| html_lines.append(f"<p><strong>Viewport:</strong> {diff.viewport}</p>") | |
| html_lines.append(f"</div>") | |
| html_lines.append("</div>") | |
| html_lines.append("</body>") | |
| html_lines.append("</html>") | |
| report_path = os.path.join(self.output_dir, "report.html") | |
| with open(report_path, 'w') as f: | |
| f.write("\n".join(html_lines)) | |
| return "\n".join(html_lines) | |
| def _get_html_styles(self) -> str: | |
| """Get CSS styles for HTML report""" | |
| return """ | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background: #f5f5f5; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| border-radius: 10px; | |
| margin-bottom: 30px; | |
| } | |
| .header h1 { | |
| margin: 0; | |
| font-size: 2.5em; | |
| } | |
| .header p { | |
| margin: 10px 0 0 0; | |
| opacity: 0.9; | |
| } | |
| .summary { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .severity { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .differences { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .difference { | |
| border-left: 4px solid #667eea; | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| background: #f9f9f9; | |
| border-radius: 5px; | |
| } | |
| .difference h3 { | |
| margin: 0 0 10px 0; | |
| color: #333; | |
| } | |
| .difference p { | |
| margin: 5px 0; | |
| color: #666; | |
| } | |
| h2 { | |
| color: #333; | |
| border-bottom: 2px solid #667eea; | |
| padding-bottom: 10px; | |
| } | |
| """ | |
| def _group_by_category(self) -> Dict[str, List[Any]]: | |
| """Group differences by category""" | |
| categories = {} | |
| visual_differences = self.state.get('visual_differences', []) | |
| for diff in visual_differences: | |
| category = diff.get('category') if isinstance(diff, dict) else getattr(diff, 'category', 'visual') | |
| if category not in categories: | |
| categories[category] = [] | |
| categories[category].append(diff) | |
| return categories | |
| def generate_all_reports(state: WorkflowState, output_dir: str): | |
| """Convenience function to generate all reports""" | |
| generator = EnhancedReportGenerator(state, output_dir) | |
| generator.generate_all_reports() | |