Files
ai-web-tester/tests/universal_tester.py
empty 1f1cc4db9a
Some checks failed
AI Web Tester CI / test (push) Has been cancelled
feat: 增强测试框架功能
主要改进:
- 新增统一测试器 (universal_tester.py) 支持多种测试模式
- 优化测试报告生成器,支持汇总报告和操作截图
- 增强探索器 DFS 算法和状态指纹识别
- 新增智能测试配置 (smart_test.yaml)
- 改进 AI 模型集成 (GLM/Gemini 支持)
- 添加开发调试工具和文档
2026-01-05 20:23:02 +08:00

355 lines
13 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
"""
通用 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()