851 lines
35 KiB
Python
851 lines
35 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
玩Android Material Design无头浏览器测试框架
|
|
使用Selenium进行自动化测试
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Tuple, Optional
|
|
|
|
# 尝试导入Selenium
|
|
try:
|
|
from selenium import webdriver
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.common.keys import Keys
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.chrome.options import Options
|
|
from selenium.common.exceptions import TimeoutException, WebDriverException
|
|
SELENIUM_AVAILABLE = True
|
|
except ImportError:
|
|
SELENIUM_AVAILABLE = False
|
|
print("⚠️ Selenium未安装,将使用备用测试方法")
|
|
|
|
# 配置日志
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.FileHandler('headless_test.log'),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class HeadlessBrowserTester:
|
|
"""无头浏览器测试器"""
|
|
|
|
def __init__(self, base_url: str = "http://localhost:8000"):
|
|
self.base_url = base_url
|
|
self.driver = None
|
|
self.test_results = []
|
|
self.screenshots_dir = Path("test_screenshots")
|
|
self.screenshots_dir.mkdir(exist_ok=True)
|
|
|
|
def setup_driver(self):
|
|
"""设置无头浏览器驱动"""
|
|
if not SELENIUM_AVAILABLE:
|
|
logger.error("Selenium不可用,无法进行无头浏览器测试")
|
|
return False
|
|
|
|
try:
|
|
chrome_options = Options()
|
|
chrome_options.add_argument("--headless") # 无头模式
|
|
chrome_options.add_argument("--no-sandbox")
|
|
chrome_options.add_argument("--disable-dev-shm-usage")
|
|
chrome_options.add_argument("--disable-gpu")
|
|
chrome_options.add_argument("--window-size=1920,1080")
|
|
chrome_options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
|
|
|
# 尝试使用系统Chrome
|
|
self.driver = webdriver.Chrome(options=chrome_options)
|
|
logger.info("✅ Chrome无头浏览器启动成功")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ 启动Chrome失败: {e}")
|
|
|
|
# 尝试使用Firefox
|
|
try:
|
|
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
|
firefox_options = FirefoxOptions()
|
|
firefox_options.add_argument("--headless")
|
|
self.driver = webdriver.Firefox(options=firefox_options)
|
|
logger.info("✅ Firefox无头浏览器启动成功")
|
|
return True
|
|
except Exception as e2:
|
|
logger.error(f"❌ 启动Firefox也失败: {e2}")
|
|
return False
|
|
|
|
def take_screenshot(self, name: str):
|
|
"""截取屏幕截图"""
|
|
if self.driver:
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
filename = self.screenshots_dir / f"{name}_{timestamp}.png"
|
|
self.driver.save_screenshot(str(filename))
|
|
logger.info(f"📸 截图保存: {filename}")
|
|
return str(filename)
|
|
return None
|
|
|
|
def record_test_result(self, module: str, test_name: str, passed: bool,
|
|
details: str = "", screenshot: str = None):
|
|
"""记录测试结果"""
|
|
result = {
|
|
"module": module,
|
|
"test_name": test_name,
|
|
"passed": passed,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"details": details,
|
|
"screenshot": screenshot
|
|
}
|
|
self.test_results.append(result)
|
|
|
|
status = "✅" if passed else "❌"
|
|
logger.info(f"{status} {module} - {test_name}: {details}")
|
|
|
|
return result
|
|
|
|
def test_homepage(self) -> Dict:
|
|
"""测试首页模块"""
|
|
module = "首页模块"
|
|
results = []
|
|
|
|
try:
|
|
# 访问首页
|
|
self.driver.get(f"{self.base_url}/")
|
|
time.sleep(2)
|
|
|
|
# 截图
|
|
screenshot = self.take_screenshot("homepage")
|
|
|
|
# 检查页面标题
|
|
title = self.driver.title
|
|
if "玩Android" in title or "Material Design" in title:
|
|
results.append(self.record_test_result(
|
|
module, "页面标题", True, f"标题: {title}", screenshot
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "页面标题", False, f"标题不符合预期: {title}", screenshot
|
|
))
|
|
|
|
# 检查Material Design元素
|
|
material_elements = [
|
|
("搜索栏", "search-container", By.CLASS_NAME),
|
|
("Banner区域", "banner", By.CLASS_NAME),
|
|
("文章列表", "article-list", By.CLASS_NAME),
|
|
("底部导航", "tab-bar", By.CLASS_NAME),
|
|
]
|
|
|
|
for element_name, selector, by_type in material_elements:
|
|
try:
|
|
element = self.driver.find_element(by_type, selector)
|
|
results.append(self.record_test_result(
|
|
module, f"{element_name}存在", True, f"找到{selector}"
|
|
))
|
|
|
|
# 检查样式
|
|
bg_color = element.value_of_css_property("background-color")
|
|
if bg_color:
|
|
results.append(self.record_test_result(
|
|
module, f"{element_name}样式", True, f"背景色: {bg_color}"
|
|
))
|
|
|
|
except Exception as e:
|
|
results.append(self.record_test_result(
|
|
module, f"{element_name}存在", False, f"未找到{selector}: {e}"
|
|
))
|
|
|
|
# 测试交互
|
|
try:
|
|
# 点击搜索栏
|
|
search_box = self.driver.find_element(By.CLASS_NAME, "search-container")
|
|
search_box.click()
|
|
time.sleep(1)
|
|
results.append(self.record_test_result(
|
|
module, "搜索栏点击", True, "搜索栏可点击"
|
|
))
|
|
except Exception as e:
|
|
results.append(self.record_test_result(
|
|
module, "搜索栏点击", False, f"搜索栏点击失败: {e}"
|
|
))
|
|
|
|
# 检查响应式
|
|
original_size = self.driver.get_window_size()
|
|
self.driver.set_window_size(375, 667) # 手机尺寸
|
|
time.sleep(1)
|
|
mobile_screenshot = self.take_screenshot("homepage_mobile")
|
|
results.append(self.record_test_result(
|
|
module, "响应式布局", True, "移动端适配正常", mobile_screenshot
|
|
))
|
|
|
|
# 恢复窗口大小
|
|
self.driver.set_window_size(original_size['width'], original_size['height'])
|
|
|
|
return {
|
|
"module": module,
|
|
"total_tests": len(results),
|
|
"passed": sum(1 for r in results if r["passed"]),
|
|
"failed": sum(1 for r in results if not r["passed"]),
|
|
"results": results
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ 首页测试失败: {e}")
|
|
return {
|
|
"module": module,
|
|
"total_tests": 0,
|
|
"passed": 0,
|
|
"failed": 1,
|
|
"error": str(e)
|
|
}
|
|
|
|
def test_material_preview(self) -> Dict:
|
|
"""测试Material Design预览模块"""
|
|
module = "Material预览模块"
|
|
results = []
|
|
|
|
try:
|
|
# 访问Material预览页面
|
|
self.driver.get(f"{self.base_url}/material_preview.html")
|
|
time.sleep(2)
|
|
|
|
# 截图
|
|
screenshot = self.take_screenshot("material_preview")
|
|
|
|
# 检查页面内容
|
|
page_text = self.driver.find_element(By.TAG_NAME, "body").text
|
|
material_keywords = ["Material Design", "颜色系统", "图标系统", "组件系统"]
|
|
|
|
for keyword in material_keywords:
|
|
if keyword in page_text:
|
|
results.append(self.record_test_result(
|
|
module, f"包含{keyword}", True, f"找到关键词: {keyword}"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, f"包含{keyword}", False, f"未找到关键词: {keyword}"
|
|
))
|
|
|
|
# 检查颜色系统
|
|
try:
|
|
color_elements = self.driver.find_elements(By.CLASS_NAME, "color-swatch")
|
|
if len(color_elements) >= 3:
|
|
results.append(self.record_test_result(
|
|
module, "颜色系统", True, f"找到{len(color_elements)}个颜色样本"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "颜色系统", False, f"颜色样本不足: {len(color_elements)}个"
|
|
))
|
|
except Exception as e:
|
|
results.append(self.record_test_result(
|
|
module, "颜色系统", False, f"颜色系统检查失败: {e}"
|
|
))
|
|
|
|
# 检查图标系统
|
|
try:
|
|
icon_elements = self.driver.find_elements(By.CLASS_NAME, "material-icon")
|
|
if len(icon_elements) >= 5:
|
|
results.append(self.record_test_result(
|
|
module, "图标系统", True, f"找到{len(icon_elements)}个Material图标"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "图标系统", False, f"图标数量不足: {len(icon_elements)}个"
|
|
))
|
|
except Exception as e:
|
|
results.append(self.record_test_result(
|
|
module, "图标系统", False, f"图标系统检查失败: {e}"
|
|
))
|
|
|
|
return {
|
|
"module": module,
|
|
"total_tests": len(results),
|
|
"passed": sum(1 for r in results if r["passed"]),
|
|
"failed": sum(1 for r in results if not r["passed"]),
|
|
"results": results
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Material预览测试失败: {e}")
|
|
return {
|
|
"module": module,
|
|
"total_tests": 0,
|
|
"passed": 0,
|
|
"failed": 1,
|
|
"error": str(e)
|
|
}
|
|
|
|
def test_api_endpoints(self) -> Dict:
|
|
"""测试API接口模块"""
|
|
module = "API接口模块"
|
|
results = []
|
|
|
|
try:
|
|
# 测试健康检查API
|
|
self.driver.get(f"{self.base_url}/health")
|
|
time.sleep(1)
|
|
|
|
# 检查JSON响应
|
|
page_text = self.driver.find_element(By.TAG_NAME, "pre").text
|
|
try:
|
|
health_data = json.loads(page_text)
|
|
if health_data.get("status") == "healthy":
|
|
results.append(self.record_test_result(
|
|
module, "健康检查API", True, "API返回健康状态"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "健康检查API", False, f"状态异常: {health_data.get('status')}"
|
|
))
|
|
except json.JSONDecodeError as e:
|
|
results.append(self.record_test_result(
|
|
module, "健康检查API", False, f"JSON解析失败: {e}"
|
|
))
|
|
|
|
# 测试Banner API
|
|
self.driver.get(f"{self.base_url}/api/banner/json")
|
|
time.sleep(1)
|
|
|
|
page_text = self.driver.find_element(By.TAG_NAME, "pre").text
|
|
try:
|
|
banner_data = json.loads(page_text)
|
|
if banner_data.get("errorCode") == 0 and "data" in banner_data:
|
|
results.append(self.record_test_result(
|
|
module, "Banner API", True, f"返回{len(banner_data['data'])}个Banner"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "Banner API", False, f"API格式错误: {banner_data}"
|
|
))
|
|
except Exception as e:
|
|
results.append(self.record_test_result(
|
|
module, "Banner API", False, f"Banner API测试失败: {e}"
|
|
))
|
|
|
|
# 测试文章列表API
|
|
self.driver.get(f"{self.base_url}/api/article/list/0/json")
|
|
time.sleep(1)
|
|
|
|
page_text = self.driver.find_element(By.TAG_NAME, "pre").text
|
|
try:
|
|
article_data = json.loads(page_text)
|
|
if article_data.get("errorCode") == 0 and "data" in article_data:
|
|
article_count = len(article_data["data"].get("datas", []))
|
|
results.append(self.record_test_result(
|
|
module, "文章列表API", True, f"返回{article_count}篇文章"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "文章列表API", False, f"API格式错误"
|
|
))
|
|
except Exception as e:
|
|
results.append(self.record_test_result(
|
|
module, "文章列表API", False, f"文章API测试失败: {e}"
|
|
))
|
|
|
|
return {
|
|
"module": module,
|
|
"total_tests": len(results),
|
|
"passed": sum(1 for r in results if r["passed"]),
|
|
"failed": sum(1 for r in results if not r["passed"]),
|
|
"results": results
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ API接口测试失败: {e}")
|
|
return {
|
|
"module": module,
|
|
"total_tests": 0,
|
|
"passed": 0,
|
|
"failed": 1,
|
|
"error": str(e)
|
|
}
|
|
|
|
def test_quick_test_page(self) -> Dict:
|
|
"""测试快速测试页面模块"""
|
|
module = "快速测试页面模块"
|
|
results = []
|
|
|
|
try:
|
|
# 访问快速测试页面
|
|
self.driver.get(f"{self.base_url}/quick_test.html")
|
|
time.sleep(2)
|
|
|
|
# 截图
|
|
screenshot = self.take_screenshot("quick_test_page")
|
|
|
|
# 检查页面结构
|
|
test_buttons = self.driver.find_elements(By.CLASS_NAME, "test-button")
|
|
if len(test_buttons) >= 4:
|
|
results.append(self.record_test_result(
|
|
module, "测试按钮", True, f"找到{len(test_buttons)}个测试按钮"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "测试按钮", False, f"测试按钮不足: {len(test_buttons)}个"
|
|
))
|
|
|
|
# 检查服务器信息显示
|
|
server_info_elements = self.driver.find_elements(By.CLASS_NAME, "info-item")
|
|
if len(server_info_elements) >= 4:
|
|
results.append(self.record_test_result(
|
|
module, "服务器信息", True, f"显示{len(server_info_elements)}项服务器信息"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "服务器信息", False, f"服务器信息不足: {len(server_info_elements)}项"
|
|
))
|
|
|
|
# 测试按钮点击(新标签页打开)
|
|
for i, button in enumerate(test_buttons[:2]): # 只测试前两个按钮
|
|
try:
|
|
button_text = button.text
|
|
button.click()
|
|
time.sleep(1)
|
|
|
|
# 切换到新标签页
|
|
if len(self.driver.window_handles) > 1:
|
|
self.driver.switch_to.window(self.driver.window_handles[1])
|
|
time.sleep(1)
|
|
|
|
# 检查新页面
|
|
new_page_title = self.driver.title
|
|
results.append(self.record_test_result(
|
|
module, f"按钮点击测试{i+1}", True,
|
|
f"按钮'{button_text}'打开页面: {new_page_title}"
|
|
))
|
|
|
|
# 关闭新标签页,返回原页面
|
|
self.driver.close()
|
|
self.driver.switch_to.window(self.driver.window_handles[0])
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, f"按钮点击测试{i+1}", False,
|
|
f"按钮'{button_text}'未打开新标签页"
|
|
))
|
|
|
|
except Exception as e:
|
|
results.append(self.record_test_result(
|
|
module, f"按钮点击测试{i+1}", False,
|
|
f"按钮点击测试失败: {e}"
|
|
))
|
|
|
|
return {
|
|
"module": module,
|
|
"total_tests": len(results),
|
|
"passed": sum(1 for r in results if r["passed"]),
|
|
"failed": sum(1 for r in results if not r["passed"]),
|
|
"results": results
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ 快速测试页面测试失败: {e}")
|
|
return {
|
|
"module": module,
|
|
"total_tests": 0,
|
|
"passed": 0,
|
|
"failed": 1,
|
|
"error": str(e)
|
|
}
|
|
|
|
def test_uniapp_pages(self) -> Dict:
|
|
"""测试Uniapp页面模块"""
|
|
module = "Uniapp页面模块"
|
|
results = []
|
|
|
|
try:
|
|
# 检查pages目录是否存在
|
|
pages_dir = Path("pages")
|
|
if pages_dir.exists() and pages_dir.is_dir():
|
|
results.append(self.record_test_result(
|
|
module, "pages目录", True, "Uniapp pages目录存在"
|
|
))
|
|
|
|
# 统计页面数量
|
|
page_folders = [d for d in pages_dir.iterdir() if d.is_dir()]
|
|
results.append(self.record_test_result(
|
|
module, "页面数量", True, f"找到{len(page_folders)}个页面目录"
|
|
))
|
|
|
|
# 检查关键页面文件
|
|
required_pages = ["index", "project", "official", "system", "mine"]
|
|
for page in required_pages:
|
|
page_file = pages_dir / page / f"{page}.vue"
|
|
if page_file.exists():
|
|
results.append(self.record_test_result(
|
|
module, f"{page}页面文件", True, f"{page}.vue文件存在"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, f"{page}页面文件", False, f"{page}.vue文件不存在"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "pages目录", False, "Uniapp pages目录不存在"
|
|
))
|
|
|
|
# 检查静态资源
|
|
static_dir = Path("static")
|
|
if static_dir.exists():
|
|
# 检查图标
|
|
icons_dir = static_dir / "images" / "icons"
|
|
if icons_dir.exists():
|
|
icon_files = list(icons_dir.glob("*.png")) + list(icons_dir.glob("*.svg"))
|
|
results.append(self.record_test_result(
|
|
module, "图标资源", True, f"找到{len(icon_files)}个图标文件"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, "图标资源", False, "图标目录不存在"
|
|
))
|
|
|
|
# 检查配置文件
|
|
config_files = ["manifest.json", "pages.json", "App.vue"]
|
|
for config_file in config_files:
|
|
if Path(config_file).exists():
|
|
results.append(self.record_test_result(
|
|
module, f"{config_file}文件", True, f"{config_file}存在"
|
|
))
|
|
else:
|
|
results.append(self.record_test_result(
|
|
module, f"{config_file}文件", False, f"{config_file}不存在"
|
|
))
|
|
|
|
return {
|
|
"module": module,
|
|
"total_tests": len(results),
|
|
"passed": sum(1 for r in results if r["passed"]),
|
|
"failed": sum(1 for r in results if not r["passed"]),
|
|
"results": results
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Uniapp页面测试失败: {e}")
|
|
return {
|
|
"module": module,
|
|
"total_tests": 0,
|
|
"passed": 0,
|
|
"failed": 1,
|
|
"error": str(e)
|
|
}
|
|
|
|
def run_all_tests(self) -> Dict:
|
|
"""运行所有测试"""
|
|
logger.info("🚀 开始无头浏览器自动化测试")
|
|
logger.info(f"📡 测试地址: {self.base_url}")
|
|
|
|
if not self.setup_driver():
|
|
logger.error("❌ 无法启动无头浏览器,测试终止")
|
|
return {"error": "无法启动无头浏览器"}
|
|
|
|
all_results = []
|
|
|
|
try:
|
|
# 运行各个模块测试
|
|
test_modules = [
|
|
("首页模块", self.test_homepage),
|
|
("Material预览模块", self.test_material_preview),
|
|
("API接口模块", self.test_api_endpoints),
|
|
("快速测试页面模块", self.test_quick_test_page),
|
|
("Uniapp页面模块", self.test_uniapp_pages),
|
|
]
|
|
|
|
for module_name, test_func in test_modules:
|
|
logger.info(f"\n🔍 开始测试: {module_name}")
|
|
result = test_func()
|
|
all_results.append(result)
|
|
logger.info(f"📊 {module_name}测试完成: {result['passed']}/{result['total_tests']} 通过")
|
|
|
|
# 生成测试报告
|
|
report = self.generate_test_report(all_results)
|
|
|
|
return report
|
|
|
|
finally:
|
|
if self.driver:
|
|
self.driver.quit()
|
|
logger.info("🛑 无头浏览器已关闭")
|
|
|
|
def generate_test_report(self, all_results: List[Dict]) -> Dict:
|
|
"""生成测试报告"""
|
|
total_tests = sum(r.get("total_tests", 0) for r in all_results)
|
|
total_passed = sum(r.get("passed", 0) for r in all_results)
|
|
total_failed = sum(r.get("failed", 0) for r in all_results)
|
|
|
|
report = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"base_url": self.base_url,
|
|
"total_modules": len(all_results),
|
|
"total_tests": total_tests,
|
|
"total_passed": total_passed,
|
|
"total_failed": total_failed,
|
|
"success_rate": (total_passed / total_tests * 100) if total_tests > 0 else 0,
|
|
"modules": all_results,
|
|
"summary": self._generate_summary(all_results)
|
|
}
|
|
|
|
# 保存报告到文件
|
|
report_file = "headless_test_report.json"
|
|
with open(report_file, "w", encoding="utf-8") as f:
|
|
json.dump(report, f, ensure_ascii=False, indent=2)
|
|
|
|
# 生成HTML报告
|
|
self._generate_html_report(report)
|
|
|
|
logger.info(f"\n🎯 测试完成!")
|
|
logger.info(f"📊 总体结果: {total_passed}/{total_tests} 通过 ({report['success_rate']:.1f}%)")
|
|
logger.info(f"📄 详细报告: {report_file}")
|
|
logger.info(f"📄 HTML报告: headless_test_report.html")
|
|
|
|
return report
|
|
|
|
def _generate_summary(self, all_results: List[Dict]) -> str:
|
|
"""生成测试摘要"""
|
|
summary_lines = []
|
|
|
|
for module_result in all_results:
|
|
module_name = module_result.get("module", "未知模块")
|
|
passed = module_result.get("passed", 0)
|
|
total = module_result.get("total_tests", 0)
|
|
|
|
if total > 0:
|
|
percentage = (passed / total) * 100
|
|
status = "✅" if percentage >= 80 else "⚠️" if percentage >= 50 else "❌"
|
|
summary_lines.append(f"{status} {module_name}: {passed}/{total} ({percentage:.1f}%)")
|
|
else:
|
|
summary_lines.append(f"❌ {module_name}: 0/0 (无测试)")
|
|
|
|
return "\n".join(summary_lines)
|
|
|
|
def _generate_html_report(self, report: Dict):
|
|
"""生成HTML测试报告"""
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>无头浏览器测试报告</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; color: #333; line-height: 1.6; padding: 20px; }}
|
|
|
|
.container {{ max-width: 1200px; margin: 0 auto; }}
|
|
|
|
.header {{ background: linear-gradient(135deg, #6200EE 0%, #3700B3 100%); color: white; padding: 40px; border-radius: 15px; margin-bottom: 30px; text-align: center; }}
|
|
.header h1 {{ font-size: 36px; margin-bottom: 10px; }}
|
|
.header .subtitle {{ font-size: 18px; opacity: 0.9; }}
|
|
|
|
.summary-card {{ background: white; border-radius: 15px; padding: 30px; margin-bottom: 30px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }}
|
|
.summary-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; }}
|
|
.summary-item {{ text-align: center; padding: 20px; border-radius: 10px; }}
|
|
.summary-item.total {{ background: #e3f2fd; }}
|
|
.summary-item.passed {{ background: #e8f5e9; }}
|
|
.summary-item.failed {{ background: #ffebee; }}
|
|
.summary-item.rate {{ background: #fff3e0; }}
|
|
.summary-value {{ font-size: 32px; font-weight: bold; margin-bottom: 5px; }}
|
|
.summary-label {{ font-size: 14px; color: #666; }}
|
|
|
|
.module-section {{ margin-bottom: 30px; }}
|
|
.module-header {{ background: #6200EE; color: white; padding: 15px 20px; border-radius: 10px 10px 0 0; display: flex; justify-content: space-between; align-items: center; }}
|
|
.module-title {{ font-size: 20px; font-weight: 500; }}
|
|
.module-stats {{ font-size: 16px; }}
|
|
|
|
.test-results {{ background: white; border-radius: 0 0 10px 10px; padding: 20px; box-shadow: 0 3px 10px rgba(0,0,0,0.1); }}
|
|
.test-item {{ padding: 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; }}
|
|
.test-item:last-child {{ border-bottom: none; }}
|
|
.test-status {{ font-size: 24px; margin-right: 15px; width: 40px; text-align: center; }}
|
|
.test-details {{ flex: 1; }}
|
|
.test-name {{ font-weight: 500; margin-bottom: 5px; }}
|
|
.test-info {{ font-size: 14px; color: #666; }}
|
|
|
|
.screenshot-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-top: 20px; }}
|
|
.screenshot-item {{ border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }}
|
|
.screenshot-item img {{ width: 100%; height: 200px; object-fit: cover; }}
|
|
.screenshot-caption {{ padding: 10px; background: #f9f9f9; font-size: 14px; }}
|
|
|
|
.footer {{ text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 14px; }}
|
|
|
|
@media (max-width: 768px) {{
|
|
.summary-grid {{ grid-template-columns: 1fr; }}
|
|
.screenshot-grid {{ grid-template-columns: 1fr; }}
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🎯 无头浏览器测试报告</h1>
|
|
<div class="subtitle">玩Android Material Design开发服务器</div>
|
|
<div class="subtitle">测试时间: {report['timestamp']}</div>
|
|
</div>
|
|
|
|
<div class="summary-card">
|
|
<h2>📊 测试概览</h2>
|
|
<div class="summary-grid">
|
|
<div class="summary-item total">
|
|
<div class="summary-value">{report['total_modules']}</div>
|
|
<div class="summary-label">测试模块</div>
|
|
</div>
|
|
<div class="summary-item total">
|
|
<div class="summary-value">{report['total_tests']}</div>
|
|
<div class="summary-label">总测试数</div>
|
|
</div>
|
|
<div class="summary-item passed">
|
|
<div class="summary-value">{report['total_passed']}</div>
|
|
<div class="summary-label">通过测试</div>
|
|
</div>
|
|
<div class="summary-item failed">
|
|
<div class="summary-value">{report['total_failed']}</div>
|
|
<div class="summary-label">失败测试</div>
|
|
</div>
|
|
<div class="summary-item rate">
|
|
<div class="summary-value">{report['success_rate']:.1f}%</div>
|
|
<div class="summary-label">通过率</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="summary-card">
|
|
<h2>🔍 测试服务器信息</h2>
|
|
<div style="margin-top: 15px;">
|
|
<p><strong>测试地址:</strong> {report['base_url']}</p>
|
|
<p><strong>测试时间:</strong> {report['timestamp']}</p>
|
|
<p><strong>测试摘要:</strong></p>
|
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-top: 10px; white-space: pre-wrap;">{report['summary']}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 style="margin: 30px 0 20px 0;">📋 详细测试结果</h2>
|
|
|
|
{self._generate_module_html(report['modules'])}
|
|
|
|
{self._generate_screenshots_html()}
|
|
|
|
<div class="footer">
|
|
<p>© 2026 玩Android Material Design测试报告 | 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
|
<p>测试框架: Selenium无头浏览器自动化测试</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
with open("headless_test_report.html", "w", encoding="utf-8") as f:
|
|
f.write(html_content)
|
|
|
|
def _generate_module_html(self, modules: List[Dict]) -> str:
|
|
"""生成模块测试结果的HTML"""
|
|
html_parts = []
|
|
|
|
for module in modules:
|
|
module_name = module.get("module", "未知模块")
|
|
passed = module.get("passed", 0)
|
|
total = module.get("total_tests", 0)
|
|
results = module.get("results", [])
|
|
|
|
# 计算通过率
|
|
percentage = (passed / total * 100) if total > 0 else 0
|
|
|
|
# 模块头部
|
|
html_parts.append(f"""
|
|
<div class="module-section">
|
|
<div class="module-header">
|
|
<div class="module-title">{module_name}</div>
|
|
<div class="module-stats">{passed}/{total} 通过 ({percentage:.1f}%)</div>
|
|
</div>
|
|
<div class="test-results">
|
|
""")
|
|
|
|
# 测试结果
|
|
for result in results:
|
|
status = "✅" if result.get("passed", False) else "❌"
|
|
test_name = result.get("test_name", "未知测试")
|
|
details = result.get("details", "")
|
|
|
|
html_parts.append(f"""
|
|
<div class="test-item">
|
|
<div class="test-status">{status}</div>
|
|
<div class="test-details">
|
|
<div class="test-name">{test_name}</div>
|
|
<div class="test-info">{details}</div>
|
|
</div>
|
|
</div>
|
|
""")
|
|
|
|
html_parts.append("""
|
|
</div>
|
|
</div>
|
|
""")
|
|
|
|
return "\n".join(html_parts)
|
|
|
|
def _generate_screenshots_html(self) -> str:
|
|
"""生成截图展示的HTML"""
|
|
screenshot_files = list(self.screenshots_dir.glob("*.png"))
|
|
|
|
if not screenshot_files:
|
|
return ""
|
|
|
|
html_parts = ['<h2 style="margin: 30px 0 20px 0;">📸 测试截图</h2><div class="screenshot-grid">']
|
|
|
|
for screenshot_file in screenshot_files[:6]: # 最多显示6张截图
|
|
filename = screenshot_file.name
|
|
html_parts.append(f"""
|
|
<div class="screenshot-item">
|
|
<img src="{screenshot_file}" alt="{filename}">
|
|
<div class="screenshot-caption">{filename}</div>
|
|
</div>
|
|
""")
|
|
|
|
html_parts.append('</div>')
|
|
return "\n".join(html_parts)
|
|
|
|
def main():
|
|
"""主函数"""
|
|
print("🚀 玩Android Material Design无头浏览器测试框架")
|
|
print("="*60)
|
|
|
|
# 检查Selenium是否可用
|
|
if not SELENIUM_AVAILABLE:
|
|
print("❌ Selenium未安装,无法进行无头浏览器测试")
|
|
print("💡 请安装: pip install selenium")
|
|
return
|
|
|
|
# 创建测试器
|
|
tester = HeadlessBrowserTester(base_url="http://localhost:8000")
|
|
|
|
# 运行测试
|
|
report = tester.run_all_tests()
|
|
|
|
# 输出结果
|
|
if "error" in report:
|
|
print(f"❌ 测试失败: {report['error']}")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"\n🎯 测试完成!")
|
|
print(f"📊 总体结果: {report['total_passed']}/{report['total_tests']} 通过 ({report['success_rate']:.1f}%)")
|
|
print(f"📄 JSON报告: headless_test_report.json")
|
|
print(f"📄 HTML报告: headless_test_report.html")
|
|
print(f"📸 截图目录: test_screenshots/")
|
|
|
|
if report['success_rate'] >= 80:
|
|
print("✅ 测试通过!")
|
|
sys.exit(0)
|
|
elif report['success_rate'] >= 50:
|
|
print("⚠️ 测试部分通过,需要改进")
|
|
sys.exit(1)
|
|
else:
|
|
print("❌ 测试失败!")
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main() |