Files
appium_ui_test/test_reporter.py
2025-10-31 17:53:12 +08:00

451 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
测试报告生成器模块
用于生成JSON和HTML格式的测试报告
"""
import json
import datetime
from pathlib import Path
from jinja2 import Template
from config import Config
class TestReporter:
"""测试报告生成器"""
def __init__(self):
self.reports_dir = Config.REPORTS_DIR
self.reports_dir.mkdir(exist_ok=True)
self.config = Config.REPORT
self.test_data = {
'timestamp': datetime.datetime.now(),
'xml_analysis': {},
'visual_comparison': {},
'performance_data': {},
'issues': [],
'test_summary': {}
}
def add_xml_analysis(self, analyzer):
"""添加XML分析结果"""
self.test_data['xml_analysis'] = {
'statistics': analyzer.get_statistics(),
'issues': analyzer.issues,
'elements_count': len(analyzer.elements_data),
'duplicate_ids': analyzer.find_duplicate_ids(),
'accessibility_issues': analyzer.get_accessibility_issues()
}
def add_visual_comparison(self, comparator):
"""添加视觉比对结果"""
if comparator.comparison_results:
result = comparator.comparison_results[-1]
summary = comparator.get_comparison_summary()
self.test_data['visual_comparison'] = {
'similarity_score': result['similarity_score'],
'diff_regions_count': len(result['diff_regions']),
'diff_percentage': result['diff_percentage'],
'total_diff_area': result['total_diff_area'],
'meets_threshold': result['meets_threshold'],
'summary': summary
}
def add_performance_data(self, data):
"""添加性能数据"""
self.test_data['performance_data'] = data
def add_test_summary(self, summary):
"""添加测试摘要"""
self.test_data['test_summary'] = summary
def generate_report(self, format=None):
"""生成测试报告"""
if format is None:
format = self.config['default_format']
timestamp = self.test_data['timestamp'].strftime("%Y%m%d_%H%M%S")
if format == 'json':
report_path = self.reports_dir / f"test_report_{timestamp}.json"
self._generate_json_report(report_path)
elif format == 'html':
report_path = self.reports_dir / f"test_report_{timestamp}.html"
self._generate_html_report(report_path)
else:
raise ValueError(f"不支持的报告格式: {format}")
print(f"📊 测试报告已生成: {report_path}")
return report_path
def _generate_json_report(self, output_path):
"""生成JSON格式报告"""
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(self.test_data, f, ensure_ascii=False, indent=2, default=str)
def _generate_html_report(self, output_path):
"""生成HTML格式报告"""
html_template = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>UI测试报告</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 { margin: 0; font-size: 2.5em; }
.header p { margin: 10px 0 0 0; opacity: 0.9; }
.section {
margin: 0;
padding: 25px;
border-bottom: 1px solid #eee;
}
.section:last-child { border-bottom: none; }
.section h2 {
color: #333;
margin-top: 0;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.issue {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 12px;
margin: 8px 0;
border-radius: 5px;
border-left: 4px solid #f39c12;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 12px;
margin: 8px 0;
border-radius: 5px;
border-left: 4px solid #28a745;
}
.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
padding: 12px;
margin: 8px 0;
border-radius: 5px;
border-left: 4px solid #dc3545;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 20px 0;
}
.stat-item {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
border: 1px solid #e9ecef;
}
.stat-item h3 {
font-size: 2em;
margin: 0;
color: #667eea;
}
.stat-item p {
margin: 5px 0 0 0;
color: #666;
font-weight: 500;
}
.progress-bar {
background: #e9ecef;
border-radius: 10px;
height: 20px;
margin: 10px 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #28a745, #20c997);
transition: width 0.3s ease;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: white;
}
th, td {
border: 1px solid #dee2e6;
padding: 12px;
text-align: left;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
tr:nth-child(even) { background: #f8f9fa; }
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
}
.badge-success { background: #d4edda; color: #155724; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-danger { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 UI测试报告</h1>
<p>生成时间: {{ timestamp }}</p>
</div>
<div class="section">
<h2>📊 测试概览</h2>
<div class="stats">
<div class="stat-item">
<h3>{{ xml_stats.total_elements or 0 }}</h3>
<p>总元素数</p>
</div>
<div class="stat-item">
<h3>{{ xml_stats.issues_count or 0 }}</h3>
<p>发现问题</p>
</div>
<div class="stat-item">
<h3>{{ visual_similarity }}%</h3>
<p>视觉相似度</p>
</div>
<div class="stat-item">
<h3>{{ performance_score }}%</h3>
<p>性能评分</p>
</div>
</div>
{% if visual_data and visual_data.similarity_score %}
<div>
<p><strong>视觉相似度进度:</strong></p>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ (visual_data.similarity_score * 100)|round }}%"></div>
</div>
<p>{{ (visual_data.similarity_score * 100)|round }}% 相似度</p>
</div>
{% endif %}
</div>
<div class="section">
<h2>📱 XML布局分析</h2>
<div class="stats">
<div class="stat-item">
<h3>{{ xml_stats.clickable_elements or 0 }}</h3>
<p>可点击元素</p>
</div>
<div class="stat-item">
<h3>{{ xml_stats.text_elements or 0 }}</h3>
<p>文本元素</p>
</div>
<div class="stat-item">
<h3>{{ xml_stats.max_depth or 0 }}</h3>
<p>最大层级深度</p>
</div>
</div>
{% if xml_issues %}
<h3>🔍 发现的问题:</h3>
{% for issue in xml_issues[:10] %}
<div class="issue">
<strong>{{ issue.element }}</strong>: {{ issue.issue }}
{% if issue.bounds %}
<small style="color: #666;"> (位置: {{ issue.bounds.x1 }},{{ issue.bounds.y1 }})</small>
{% endif %}
</div>
{% endfor %}
{% if xml_issues|length > 10 %}
<p><em>... 还有 {{ xml_issues|length - 10 }} 个问题未显示</em></p>
{% endif %}
{% else %}
<div class="success">
✅ 未发现XML布局问题
</div>
{% endif %}
</div>
<div class="section">
<h2>👁️ 视觉比对分析</h2>
{% if visual_data %}
<div class="stats">
<div class="stat-item">
<h3>{{ "%.1f"|format(visual_data.similarity_score * 100) }}%</h3>
<p>相似度得分</p>
</div>
<div class="stat-item">
<h3>{{ visual_data.diff_regions_count }}</h3>
<p>差异区域数量</p>
</div>
<div class="stat-item">
<h3>{{ visual_data.total_diff_area }}</h3>
<p>差异面积(像素)</p>
</div>
</div>
{% if visual_data.meets_threshold %}
<div class="success">
✅ 视觉效果良好,与设计图高度一致
<span class="badge badge-success">通过阈值检查</span>
</div>
{% else %}
<div class="issue">
⚠️ 视觉差异较大 ({{ "%.1f"|format(visual_data.diff_percentage) }}%)建议检查UI实现
<span class="badge badge-warning">未通过阈值检查</span>
</div>
{% endif %}
{% else %}
<div class="issue">
❌ 未进行视觉比对分析
</div>
{% endif %}
</div>
<div class="section">
<h2>⚡ 性能数据</h2>
{% if performance_data and performance_data.operations %}
<table>
<thead>
<tr>
<th>操作类型</th>
<th>执行时间(秒)</th>
<th>时间戳</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for op in performance_data.operations[-10:] %}
<tr>
<td>{{ op.type }}</td>
<td>{{ "%.3f"|format(op.time) }}</td>
<td>{{ op.timestamp }}</td>
<td>
{% if op.time < 1.0 %}
<span class="badge badge-success">快速</span>
{% elif op.time < 3.0 %}
<span class="badge badge-warning">正常</span>
{% else %}
<span class="badge badge-danger">慢</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="issue">
❌ 无性能数据记录
</div>
{% endif %}
</div>
<div class="section">
<h2>📋 建议和总结</h2>
{% if xml_stats.issues_count and xml_stats.issues_count > 0 %}
<div class="issue">
🔧 发现 {{ xml_stats.issues_count }} 个布局问题,建议优化可访问性和用户体验
</div>
{% endif %}
{% if visual_data and visual_data.diff_percentage > 10 %}
<div class="error">
🎨 视觉差异超过10%建议检查UI实现是否符合设计要求
</div>
{% endif %}
{% if performance_data and performance_data.operations %}
{% set avg_time = (performance_data.operations | map(attribute='time') | sum) / (performance_data.operations | length) %}
{% if avg_time > 2.0 %}
<div class="issue">
⚡ 平均操作时间较长 ({{ "%.2f"|format(avg_time) }}秒),建议优化性能
</div>
{% endif %}
{% endif %}
<div class="success">
✅ 测试完成详细数据已记录。建议定期进行UI测试以确保应用质量。
</div>
</div>
</div>
</body>
</html>
"""
template = Template(html_template)
xml_stats = self.test_data.get('xml_analysis', {}).get('statistics', {})
visual_data = self.test_data.get('visual_comparison', {})
performance_data = self.test_data.get('performance_data', {})
# 计算性能评分
performance_score = 100
if performance_data.get('operations'):
avg_time = sum(op.get('time', 0) for op in performance_data['operations']) / len(performance_data['operations'])
if avg_time > 3.0:
performance_score = 60
elif avg_time > 1.0:
performance_score = 80
html_content = template.render(
timestamp=self.test_data['timestamp'].strftime("%Y-%m-%d %H:%M:%S"),
xml_stats=xml_stats,
xml_issues=self.test_data.get('xml_analysis', {}).get('issues', []),
visual_data=visual_data,
performance_data=performance_data,
visual_similarity=f"{visual_data.get('similarity_score', 0) * 100:.1f}" if visual_data else "N/A",
performance_score=performance_score
)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
def get_report_summary(self):
"""获取报告摘要"""
xml_stats = self.test_data.get('xml_analysis', {}).get('statistics', {})
visual_data = self.test_data.get('visual_comparison', {})
summary = {
'total_elements': xml_stats.get('total_elements', 0),
'issues_found': xml_stats.get('issues_count', 0),
'visual_similarity': visual_data.get('similarity_score', 0) * 100 if visual_data else 0,
'test_passed': xml_stats.get('issues_count', 0) == 0 and visual_data.get('meets_threshold', False)
}
return summary