Some checks failed
AI Web Tester CI / test (push) Has been cancelled
主要改进: - 新增统一测试器 (universal_tester.py) 支持多种测试模式 - 优化测试报告生成器,支持汇总报告和操作截图 - 增强探索器 DFS 算法和状态指纹识别 - 新增智能测试配置 (smart_test.yaml) - 改进 AI 模型集成 (GLM/Gemini 支持) - 添加开发调试工具和文档
355 lines
13 KiB
Python
355 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
通用 Web 测试框架
|
||
支持测试任意网站
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import json
|
||
import yaml
|
||
from typing import Dict, Any, List, Optional
|
||
from dataclasses import dataclass, field
|
||
sys.path.insert(0, ".")
|
||
|
||
from src import WebTester
|
||
|
||
|
||
@dataclass
|
||
class TestConfig:
|
||
"""测试配置"""
|
||
url: str
|
||
name: str = "Web Test"
|
||
mode: str = "explore" # explore, goal, hybrid
|
||
headless: bool = True
|
||
model: str = "claude"
|
||
|
||
# 登录配置
|
||
login: Optional[Dict[str, Any]] = None
|
||
# 示例:
|
||
# login: {
|
||
# "url": "http://example.com/login",
|
||
# "username": "user@example.com",
|
||
# "password": "password",
|
||
# "username_field": "email",
|
||
# "password_field": "password",
|
||
# "submit_button": "登录"
|
||
# }
|
||
|
||
# 探索配置
|
||
explore_config: Dict[str, Any] = field(default_factory=lambda: {
|
||
"max_depth": 20,
|
||
"max_clicks": 100,
|
||
"focus_patterns": [],
|
||
"dangerous_patterns": ["删除", "delete", "退出", "exit", "注销", "logout"]
|
||
})
|
||
|
||
# 测试步骤(hybrid模式)
|
||
steps: List[Dict[str, Any]] = field(default_factory=list)
|
||
# 示例:
|
||
# steps: [
|
||
# {"action": "goal", "goal": "点击登录按钮"},
|
||
# {"action": "explore", "config": {"max_clicks": 10}},
|
||
# {"action": "verify", "target": "显示登录成功"}
|
||
# ]
|
||
|
||
# 验证规则
|
||
verifications: List[Dict[str, Any]] = field(default_factory=list)
|
||
# 示例:
|
||
# verifications: [
|
||
# {"type": "url_contains", "value": "/dashboard"},
|
||
# {"type": "element_exists", "selector": ".user-profile"},
|
||
# {"type": "text_contains", "text": "欢迎"}
|
||
# ]
|
||
|
||
|
||
class UniversalWebTester:
|
||
"""通用 Web 测试器"""
|
||
|
||
def __init__(self, config: TestConfig):
|
||
self.config = config
|
||
# 优先使用配置中的模型,如果没有则使用默认
|
||
model_name = getattr(config, "model", "glm")
|
||
print(f"DEBUG: Initializing WebTester with model: {model_name}")
|
||
self.tester = WebTester(model=model_name, headless=config.headless)
|
||
|
||
def run(self) -> Dict[str, Any]:
|
||
"""运行测试"""
|
||
result = {
|
||
"name": self.config.name,
|
||
"url": self.config.url,
|
||
"status": "passed",
|
||
"steps": [],
|
||
"errors": []
|
||
}
|
||
|
||
try:
|
||
# 启动浏览器
|
||
self.tester.start()
|
||
self.tester.goto(self.config.url)
|
||
|
||
# 处理登录
|
||
if self.config.login:
|
||
self._handle_login()
|
||
|
||
# 根据模式执行测试
|
||
if self.config.mode == "explore":
|
||
self._run_explore(result)
|
||
elif self.config.mode == "goal":
|
||
self._run_goal(result)
|
||
elif self.config.mode == "hybrid":
|
||
self._run_hybrid(result)
|
||
|
||
# 执行验证
|
||
self._run_verifications(result)
|
||
|
||
except Exception as e:
|
||
result["status"] = "failed"
|
||
result["errors"].append(str(e))
|
||
finally:
|
||
# 生成汇总报告
|
||
try:
|
||
from src.reporter.generator import ReportGenerator
|
||
reporter = ReportGenerator()
|
||
report_path = reporter.generate_session_report(self.config.name, result)
|
||
result["report_path"] = str(report_path)
|
||
print(f"\n📊 测试报告已生成: {report_path}")
|
||
except Exception as e:
|
||
print(f"\n⚠️ 报告生成失败: {e}")
|
||
|
||
self.tester.stop()
|
||
|
||
return result
|
||
|
||
def _handle_login(self):
|
||
"""处理登录"""
|
||
login_config = self.config.login
|
||
|
||
# 如果提供了登录URL,先跳转
|
||
if login_config.get("url"):
|
||
self.tester.goto(login_config["url"])
|
||
|
||
# 构建登录目标
|
||
username = login_config["username"]
|
||
password = login_config["password"]
|
||
username_field = login_config.get("username_field", "username")
|
||
password_field = login_config.get("password_field", "password")
|
||
submit_button = login_config.get("submit_button", "登录")
|
||
|
||
goal = f"在{username_field}输入框中输入{username},在{password_field}输入框中输入{password},点击{submit_button}按钮"
|
||
|
||
# 执行登录
|
||
self.tester.test(goal)
|
||
self.tester.browser.wait(2000)
|
||
|
||
def _run_explore(self, result: Dict[str, Any]):
|
||
"""运行探索模式"""
|
||
explore_result = self.tester.explore(self.config.explore_config)
|
||
result["steps"].append({
|
||
"action": "explore",
|
||
"result": explore_result
|
||
})
|
||
|
||
def _run_goal(self, result: Dict[str, Any]):
|
||
"""运行目标模式"""
|
||
# 这里可以添加具体的目标测试逻辑
|
||
pass
|
||
|
||
def _run_hybrid(self, result: Dict[str, Any]):
|
||
"""运行混合模式"""
|
||
for step in self.config.steps:
|
||
action = step.get("action")
|
||
|
||
if action == "goal":
|
||
try:
|
||
goal_result = self.tester.test(step.get("goal", ""))
|
||
result["steps"].append({
|
||
"action": "goal",
|
||
"goal": step.get("goal"),
|
||
"result": goal_result
|
||
})
|
||
except Exception as e:
|
||
# 如果goal失败,记录但不中断测试
|
||
print(f" ⚠️ 目标执行失败: {str(e)[:50]}...")
|
||
result["steps"].append({
|
||
"action": "goal",
|
||
"goal": step.get("goal"),
|
||
"result": {"success": False, "error": str(e)}
|
||
})
|
||
elif action == "explore":
|
||
explore_config = step.get("config", {})
|
||
try:
|
||
explore_result = self.tester.explore(explore_config)
|
||
result["steps"].append({
|
||
"action": "explore",
|
||
"result": explore_result
|
||
})
|
||
except Exception as e:
|
||
# 如果explore失败,尝试基础探索
|
||
print(f" ⚠️ AI探索失败: {str(e)[:50]}...")
|
||
print(" 🔄 尝试基础探索...")
|
||
try:
|
||
# 基础探索:点击可见元素
|
||
page = self.tester.browser.page
|
||
clickable = page.locator("button, a, [role='button']")
|
||
clicked = 0
|
||
|
||
for i in range(min(clickable.count(), 10)):
|
||
try:
|
||
elem = clickable.nth(i)
|
||
if elem.is_visible():
|
||
elem.click()
|
||
self.tester.browser.wait(500)
|
||
page.go_back()
|
||
self.tester.browser.wait(500)
|
||
clicked += 1
|
||
except:
|
||
continue
|
||
|
||
result["steps"].append({
|
||
"action": "explore",
|
||
"result": {
|
||
"success": True,
|
||
"click_count": clicked,
|
||
"mode": "basic"
|
||
}
|
||
})
|
||
print(f" ✅ 基础探索完成,点击了 {clicked} 个元素")
|
||
except Exception as e2:
|
||
result["steps"].append({
|
||
"action": "explore",
|
||
"result": {"success": False, "error": str(e2)}
|
||
})
|
||
elif action == "wait":
|
||
duration = step.get("duration", 1000)
|
||
self.tester.browser.wait(duration)
|
||
result["steps"].append({
|
||
"action": "wait",
|
||
"duration": duration
|
||
})
|
||
elif action == "verify":
|
||
# 执行验证
|
||
target = step.get("target", "")
|
||
verify_result = self.tester.verify(target)
|
||
result["steps"].append({
|
||
"action": "verify",
|
||
"target": target,
|
||
"result": verify_result
|
||
})
|
||
|
||
def _run_verifications(self, result: Dict[str, Any]):
|
||
"""运行验证规则"""
|
||
page = self.tester.browser.page
|
||
if not page:
|
||
return
|
||
|
||
for verification in self.config.verifications:
|
||
v_type = verification.get("type")
|
||
value = verification.get("value")
|
||
|
||
try:
|
||
if v_type == "url_contains":
|
||
if value not in page.url:
|
||
result["status"] = "failed"
|
||
result["errors"].append(f"URL不包含{value}")
|
||
|
||
elif v_type == "element_exists":
|
||
if page.locator(value).count() == 0:
|
||
result["status"] = "failed"
|
||
result["errors"].append(f"元素不存在: {value}")
|
||
|
||
elif v_type == "text_contains":
|
||
if not page.locator(f"text={value}").count():
|
||
result["status"] = "failed"
|
||
result["errors"].append(f"页面不包含文本: {value}")
|
||
|
||
except Exception as e:
|
||
result["errors"].append(f"验证失败: {e}")
|
||
|
||
|
||
def load_config_from_file(file_path: str) -> TestConfig:
|
||
"""从文件加载配置"""
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
if file_path.endswith('.yaml') or file_path.endswith('.yml'):
|
||
data = yaml.safe_load(f)
|
||
else:
|
||
data = json.load(f)
|
||
|
||
# 提取顶层配置,确保所有字段都被正确映射
|
||
config_dict = {
|
||
"url": data.get("url"),
|
||
"name": data.get("name", "Web Test"),
|
||
"mode": data.get("mode", "explore"),
|
||
"headless": data.get("headless", True),
|
||
"model": data.get("model", "glm"),
|
||
"login": data.get("login"),
|
||
"explore_config": data.get("explore_config", {}),
|
||
"steps": data.get("steps", []),
|
||
"verifications": data.get("verifications", [])
|
||
}
|
||
|
||
return TestConfig(**config_dict)
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description="通用 Web 测试工具")
|
||
parser.add_argument("--config", "-c", help="配置文件路径 (JSON/YAML)")
|
||
parser.add_argument("--url", "-u", help="要测试的URL")
|
||
parser.add_argument("--mode", "-m", choices=["explore", "goal", "hybrid"],
|
||
help="测试模式")
|
||
parser.add_argument("--model", choices=["claude", "openai", "glm", "mimo"],
|
||
help="AI模型")
|
||
parser.add_argument("--headless", action="store_true", help="无头模式")
|
||
parser.add_argument("--output", "-o", help="输出报告路径")
|
||
|
||
args = parser.parse_args()
|
||
|
||
# 加载配置
|
||
if args.config:
|
||
config = load_config_from_file(args.config)
|
||
# 如果命令行提供了参数,覆盖配置文件中的设置
|
||
if args.url: config.url = args.url
|
||
if args.mode: config.mode = args.mode
|
||
if args.model: config.model = args.model
|
||
if args.headless: config.headless = True
|
||
else:
|
||
# 使用命令行参数创建配置
|
||
if not args.url:
|
||
print("错误: 必须提供 --url 或 --config")
|
||
sys.exit(1)
|
||
config = TestConfig(
|
||
url=args.url,
|
||
mode=args.mode or "explore",
|
||
model=args.model or "glm",
|
||
headless=args.headless,
|
||
name=f"Test_{args.url.replace('://', '_').replace('/', '_')}"
|
||
)
|
||
|
||
# 运行测试
|
||
tester = UniversalWebTester(config)
|
||
result = tester.run()
|
||
|
||
# 输出结果
|
||
print("\n" + "="*50)
|
||
print(f"测试名称: {result['name']}")
|
||
print(f"测试URL: {result['url']}")
|
||
print(f"测试状态: {'✅ 通过' if result['status'] == 'passed' else '❌ 失败'}")
|
||
|
||
if result['errors']:
|
||
print("\n错误信息:")
|
||
for error in result['errors']:
|
||
print(f" - {error}")
|
||
|
||
# 保存报告
|
||
if args.output:
|
||
with open(args.output, 'w', encoding='utf-8') as f:
|
||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||
print(f"\n报告已保存到: {args.output}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|