主要改进: - 新增统一测试器 (universal_tester.py) 支持多种测试模式 - 优化测试报告生成器,支持汇总报告和操作截图 - 增强探索器 DFS 算法和状态指纹识别 - 新增智能测试配置 (smart_test.yaml) - 改进 AI 模型集成 (GLM/Gemini 支持) - 添加开发调试工具和文档
This commit is contained in:
188
tests/README.md
Normal file
188
tests/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 通用 Web 测试框架使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
这个通用测试框架可以测试任意网站,不再局限于特定系统。支持多种测试模式和配置方式。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 最简单的使用方式
|
||||
|
||||
```bash
|
||||
# 测试任意网站(默认使用 claude 模型)
|
||||
python tests/quick_test.py https://github.com
|
||||
|
||||
# 使用其他模型
|
||||
python tests/quick_test.py https://github.com --model glm
|
||||
|
||||
# 无头模式(不显示浏览器窗口)
|
||||
python tests/quick_test.py https://github.com --headless
|
||||
|
||||
# 限制点击次数
|
||||
python tests/quick_test.py https://github.com --max-clicks 20
|
||||
|
||||
# 需要登录的测试
|
||||
python tests/quick_test.py https://example.com --login --username user@example.com --password yourpassword
|
||||
```
|
||||
|
||||
### 2. 使用配置文件
|
||||
|
||||
#### JSON 配置示例
|
||||
|
||||
```bash
|
||||
# 使用 JSON 配置文件
|
||||
python tests/universal_tester.py --config tests/configs/github_example.json
|
||||
```
|
||||
|
||||
#### YAML 配置示例
|
||||
|
||||
```bash
|
||||
# 使用 YAML 配置文件
|
||||
python tests/universal_tester.py --config tests/configs/enterprise_system.yaml
|
||||
```
|
||||
|
||||
### 3. 命令行参数
|
||||
|
||||
```bash
|
||||
python tests/universal_tester.py --url https://example.com --mode explore --model claude --headless --output report.json
|
||||
```
|
||||
|
||||
## 配置文件格式
|
||||
|
||||
### 基本配置
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "测试名称",
|
||||
"url": "https://example.com",
|
||||
"mode": "explore", // explore, goal, hybrid
|
||||
"model": "claude", // claude, openai, glm, mimo
|
||||
"headless": true
|
||||
}
|
||||
```
|
||||
|
||||
### 登录配置
|
||||
|
||||
```json
|
||||
{
|
||||
"login": {
|
||||
"url": "https://example.com/login",
|
||||
"username": "user@example.com",
|
||||
"password": "password",
|
||||
"username_field": "email",
|
||||
"password_field": "password",
|
||||
"submit_button": "登录"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 探索配置
|
||||
|
||||
```json
|
||||
{
|
||||
"explore_config": {
|
||||
"max_depth": 20,
|
||||
"max_clicks": 100,
|
||||
"focus_patterns": ["管理", "设置", "新增"],
|
||||
"dangerous_patterns": ["删除", "退出", "注销"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 混合模式步骤
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "hybrid",
|
||||
"steps": [
|
||||
{"action": "goal", "goal": "点击登录按钮"},
|
||||
{"action": "wait", "duration": 2000},
|
||||
{"action": "explore", "config": {"max_clicks": 10}},
|
||||
{"action": "verify", "target": "显示登录成功"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 验证规则
|
||||
|
||||
```json
|
||||
{
|
||||
"verifications": [
|
||||
{"type": "url_contains", "value": "/dashboard"},
|
||||
{"type": "element_exists", "value": ".user-profile"},
|
||||
{"type": "text_contains", "value": "欢迎"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 测试模式说明
|
||||
|
||||
### 1. Explore 模式(探索模式)
|
||||
- AI 自动探索页面功能
|
||||
- 发现可交互元素
|
||||
- 适合了解新网站
|
||||
|
||||
### 2. Goal 模式(目标模式)
|
||||
- 执行特定目标
|
||||
- 适合单一任务测试
|
||||
|
||||
### 3. Hybrid 模式(混合模式)
|
||||
- 结合目标导向和智能探索
|
||||
- 支持多步骤业务流程测试
|
||||
|
||||
## 实际使用示例
|
||||
|
||||
### 测试 GitHub
|
||||
|
||||
```bash
|
||||
# 快速测试
|
||||
python tests/quick_test.py https://github.com --headless
|
||||
|
||||
# 使用配置文件
|
||||
python tests/universal_tester.py --config tests/configs/github_example.json
|
||||
```
|
||||
|
||||
### 测试企业系统
|
||||
|
||||
```yaml
|
||||
# enterprise_system.yaml
|
||||
name: 企业系统测试
|
||||
url: "https://your-system.com"
|
||||
mode: hybrid
|
||||
login:
|
||||
username: "test@company.com"
|
||||
password: "password"
|
||||
steps:
|
||||
- action: goal
|
||||
goal: "点击登录按钮"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 20
|
||||
```
|
||||
|
||||
```bash
|
||||
python tests/universal_tester.py --config enterprise_system.yaml
|
||||
```
|
||||
|
||||
## 报告输出
|
||||
|
||||
测试完成后会生成详细的测试报告,包括:
|
||||
- 测试步骤
|
||||
- 发现的元素
|
||||
- 错误信息
|
||||
- 验证结果
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **登录信息**:不要在配置文件中硬编码敏感信息,建议使用命令行参数或环境变量
|
||||
2. **网站兼容性**:不同网站可能需要调整定位策略
|
||||
3. **测试频率**:避免过于频繁的测试,以免被网站封禁
|
||||
4. **法律合规**:确保你有权限测试目标网站
|
||||
|
||||
## 扩展开发
|
||||
|
||||
如需添加自定义功能,可以:
|
||||
1. 继承 `UniversalWebTester` 类
|
||||
2. 添加新的验证类型
|
||||
3. 扩展配置选项
|
||||
4. 自定义报告格式
|
||||
149
tests/auto_test.py
Normal file
149
tests/auto_test.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
零配置智能测试 - 无需预先了解系统功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
from src import WebTester
|
||||
|
||||
def auto_discover_and_test(url: str, model: str = "glm", headless: bool = False):
|
||||
"""
|
||||
自动发现并测试系统功能
|
||||
适合完全未知的系统
|
||||
"""
|
||||
|
||||
print("=" * 60)
|
||||
print("🤖 零配置智能测试")
|
||||
print("=" * 60)
|
||||
print(f"🌐 目标URL: {url}")
|
||||
print(f"🤖 AI模型: {model}")
|
||||
print(f"🖥️ 无头模式: {'是' if headless else '否'}")
|
||||
print("-" * 60)
|
||||
|
||||
tester = WebTester(model=model, headless=headless)
|
||||
|
||||
try:
|
||||
# 启动并导航
|
||||
tester.start()
|
||||
tester.goto(url)
|
||||
|
||||
# 步骤1: 智能登录(如果需要)
|
||||
print("\n📝 步骤1: 检测登录状态")
|
||||
current_url = tester.browser.page.url
|
||||
|
||||
# 简单判断是否在登录页
|
||||
if "login" in current_url.lower() or tester.browser.page.locator("input[type='password']").count() > 0:
|
||||
print(" 检测到登录页面,尝试智能登录...")
|
||||
# 尝试常见的用户名密码
|
||||
login_goals = [
|
||||
"输入用户名admin和密码password,点击登录",
|
||||
"输入admin/admin,点击登录",
|
||||
"输入test/123456,点击登录"
|
||||
]
|
||||
|
||||
login_success = False
|
||||
for goal in login_goals:
|
||||
try:
|
||||
result = tester.test(goal)
|
||||
tester.browser.wait(2000)
|
||||
new_url = tester.browser.page.url
|
||||
if new_url != current_url and "login" not in new_url.lower():
|
||||
print(f" ✅ 登录成功: {goal}")
|
||||
login_success = True
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not login_success:
|
||||
print(" ⚠️ 自动登录失败,继续探索...")
|
||||
|
||||
# 步骤2: 全面探索
|
||||
print("\n🔍 步骤2: 开始智能探索")
|
||||
explore_config = {
|
||||
"max_depth": 3, # 适中的深度
|
||||
"max_clicks": 100, # 充足的点击
|
||||
"focus_patterns": [], # 不设限制,让AI自由发现
|
||||
"dangerous_patterns": ["删除", "退出", "注销", "delete", "logout", "exit"]
|
||||
}
|
||||
|
||||
explore_result = tester.explore(explore_config)
|
||||
|
||||
# 步骤3: 分析发现的功能
|
||||
print("\n📊 步骤3: 分析测试结果")
|
||||
action_log = explore_result.get("action_log", [])
|
||||
|
||||
# 统计功能类型
|
||||
clicked_elements = []
|
||||
forms_filled = []
|
||||
pages_visited = set()
|
||||
|
||||
for action in action_log:
|
||||
if action.get("action_taken"):
|
||||
element_name = action.get("element_name", "")
|
||||
clicked_elements.append(element_name)
|
||||
|
||||
if action.get("action_type") == "form_input":
|
||||
forms_filled.append(element_name)
|
||||
|
||||
if action.get("url_changed"):
|
||||
pages_visited.add(action.get("after_url", ""))
|
||||
|
||||
# 输出发现的功能
|
||||
print(f"\n✅ 测试完成!发现的功能:")
|
||||
print(f" 🖱️ 点击的元素: {len(clicked_elements)} 个")
|
||||
print(f" 📝 填写的表单: {len(forms_filled)} 个")
|
||||
print(f" 📄 访问的页面: {len(pages_visited)} 个")
|
||||
|
||||
# 显示主要功能模块
|
||||
if clicked_elements:
|
||||
print(f"\n🎯 主要功能模块:")
|
||||
unique_elements = list(set(clicked_elements))[:10] # 显示前10个
|
||||
for i, elem in enumerate(unique_elements, 1):
|
||||
print(f" {i}. {elem}")
|
||||
|
||||
# 生成简化报告
|
||||
report = {
|
||||
"url": url,
|
||||
"total_clicks": len(clicked_elements),
|
||||
"forms_filled": len(forms_filled),
|
||||
"pages_visited": len(pages_visited),
|
||||
"discovered_elements": unique_elements,
|
||||
"success": True
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
finally:
|
||||
tester.stop()
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="零配置智能测试工具")
|
||||
parser.add_argument("url", help="要测试的网站URL")
|
||||
parser.add_argument("--model", default="glm",
|
||||
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()
|
||||
|
||||
# 运行测试
|
||||
result = auto_discover_and_test(args.url, args.model, args.headless)
|
||||
|
||||
# 保存报告
|
||||
if args.output and result.get("success"):
|
||||
import json
|
||||
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()
|
||||
183
tests/configs/enterprise_system.yaml
Normal file
183
tests/configs/enterprise_system.yaml
Normal file
@@ -0,0 +1,183 @@
|
||||
name: 企业系统全功能测试
|
||||
url: "http://47.99.105.253:8084"
|
||||
mode: hybrid
|
||||
model: glm
|
||||
headless: false
|
||||
|
||||
login:
|
||||
username: "admin"
|
||||
password: "password"
|
||||
submit_button: "登录"
|
||||
|
||||
# 探索配置 - 全面测试所有功能
|
||||
explore_config:
|
||||
max_depth: 5 # 探索深度,确保覆盖所有层级
|
||||
max_clicks: 200 # 充足的点击次数
|
||||
focus_patterns: # 引导 AI 重点测试这些功能
|
||||
- "管理"
|
||||
- "设置"
|
||||
- "新增"
|
||||
- "编辑"
|
||||
- "查询"
|
||||
- "审核"
|
||||
- "提交"
|
||||
- "导出"
|
||||
- "详情"
|
||||
- "列表"
|
||||
dangerous_patterns: # 发现但不点击的危险操作
|
||||
- "删除"
|
||||
- "退出"
|
||||
- "注销"
|
||||
- "重置"
|
||||
- "清空"
|
||||
|
||||
# 测试步骤 - 覆盖主要业务流程
|
||||
steps:
|
||||
# 1. 登录验证
|
||||
- action: goal
|
||||
goal: "在登录页面输入用户名admin和密码password,点击登录按钮"
|
||||
- action: wait
|
||||
duration: 2000
|
||||
- action: verify
|
||||
target: "成功登录并进入系统主页"
|
||||
|
||||
# 2. 测试主导航菜单
|
||||
- action: goal
|
||||
goal: "点击立项论证管理菜单"
|
||||
- action: wait
|
||||
duration: 1000
|
||||
- action: verify
|
||||
target: "成功进入立项论证管理页面"
|
||||
|
||||
# 3. 测试列表和详情
|
||||
- action: goal
|
||||
goal: "点击项目输入子菜单"
|
||||
- action: wait
|
||||
duration: 1000
|
||||
- action: goal
|
||||
goal: "点击1技术协议及科研合同评审记录"
|
||||
- action: verify
|
||||
target: "显示技术协议列表"
|
||||
|
||||
# 4. 测试表单功能
|
||||
- action: goal
|
||||
goal: "点击新增按钮"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 30 # 充分测试表单填写
|
||||
max_depth: 1
|
||||
- action: verify
|
||||
target: "表单成功提交并显示成功提示"
|
||||
|
||||
# 5. 测试产品方案管理
|
||||
- action: goal
|
||||
goal: "点击产品方案管理菜单"
|
||||
- action: wait
|
||||
duration: 1000
|
||||
- action: goal
|
||||
goal: "展开研制方案子菜单"
|
||||
- action: wait
|
||||
duration: 1000
|
||||
- action: goal
|
||||
goal: "点击研制方案菜单项"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 20
|
||||
max_depth: 2
|
||||
- action: verify
|
||||
target: "成功进入研制方案页面"
|
||||
|
||||
# 6. 测试其他管理模块
|
||||
- action: goal
|
||||
goal: "点击产品初样管理"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 15
|
||||
max_depth: 1
|
||||
- action: goal
|
||||
goal: "点击产品正样管理"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 15
|
||||
max_depth: 1
|
||||
- action: goal
|
||||
goal: "点击产品定型管理"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 15
|
||||
max_depth: 1
|
||||
|
||||
# 7. 测试系统管理功能
|
||||
- action: goal
|
||||
goal: "点击系统管理菜单"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 20
|
||||
max_depth: 2
|
||||
focus_patterns: ["用户", "角色", "权限", "日志"]
|
||||
|
||||
# 8. 测试待办事项和审批
|
||||
- action: goal
|
||||
goal: "点击待办事项"
|
||||
- action: wait
|
||||
duration: 1000
|
||||
- action: goal
|
||||
goal: "点击最新的待办记录"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 15
|
||||
max_depth: 1
|
||||
- action: verify
|
||||
target: "成功查看待办详情"
|
||||
|
||||
# 9. 测试搜索和筛选
|
||||
- action: goal
|
||||
goal: "在列表页面查找搜索框并输入测试内容"
|
||||
- action: explore
|
||||
config:
|
||||
max_clicks: 10
|
||||
max_depth: 1
|
||||
|
||||
# 10. 测试导出功能
|
||||
- action: goal
|
||||
goal: "查找并点击导出按钮(如果存在)"
|
||||
- action: wait
|
||||
duration: 2000
|
||||
|
||||
# 验证规则 - 确保功能正常
|
||||
verifications:
|
||||
# 登录成功验证
|
||||
- type: url_not_contains
|
||||
value: "/login"
|
||||
- type: text_contains
|
||||
value: "立项论证"
|
||||
|
||||
# 主要功能模块验证
|
||||
- type: element_exists
|
||||
value: ".ant-menu"
|
||||
- type: element_exists
|
||||
value: "button"
|
||||
- type: element_exists
|
||||
value: "input"
|
||||
|
||||
# 表单功能验证
|
||||
- type: element_exists
|
||||
value: "input[type='text']"
|
||||
- type: element_exists
|
||||
value: "input[type='password']"
|
||||
- type: element_exists
|
||||
value: "button:has-text('提交')"
|
||||
- type: element_exists
|
||||
value: "button:has-text('保存')"
|
||||
|
||||
# 列表功能验证
|
||||
- type: element_exists
|
||||
value: "table"
|
||||
- type: element_exists
|
||||
value: ".ant-table"
|
||||
|
||||
# 弹窗功能验证
|
||||
- type: element_exists
|
||||
value: ".ant-modal"
|
||||
- type: element_exists
|
||||
value: ".ant-drawer"
|
||||
23
tests/configs/github_example.json
Normal file
23
tests/configs/github_example.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "GitHub 探索测试",
|
||||
"url": "https://github.com",
|
||||
"mode": "explore",
|
||||
"model": "claude",
|
||||
"headless": true,
|
||||
"explore_config": {
|
||||
"max_depth": 10,
|
||||
"max_clicks": 50,
|
||||
"focus_patterns": ["repository", "code", "pull request", "issue"],
|
||||
"dangerous_patterns": ["delete", "remove", "sign out"]
|
||||
},
|
||||
"verifications": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "header[role='banner']"
|
||||
},
|
||||
{
|
||||
"type": "text_contains",
|
||||
"value": "Where the world builds software"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
tests/configs/simple_test.yaml
Normal file
11
tests/configs/simple_test.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
# 极简测试配置 - 适合未知系统
|
||||
name: 快速功能测试
|
||||
url: "http://47.99.105.253:8084"
|
||||
mode: explore # 只用探索模式,让AI自由发现
|
||||
|
||||
# 简单的探索配置
|
||||
explore_config:
|
||||
max_depth: 30 # 适中的深度
|
||||
max_clicks: 2000 # 快速测试
|
||||
# 不设置 focus_patterns,让AI自由发现
|
||||
dangerous_patterns: ["退出", "注销"] # 只避开危险操作
|
||||
32
tests/configs/smart_test.yaml
Normal file
32
tests/configs/smart_test.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# 智能配置 - 自动适应登录状态
|
||||
name: 智能适应测试
|
||||
url: "http://47.99.105.253:8084"
|
||||
mode: hybrid
|
||||
model: glm
|
||||
|
||||
# 测试步骤 - 根据登录状态自动调整
|
||||
steps:
|
||||
# 步骤1: 显式登录逻辑
|
||||
- action: goal
|
||||
goal: "在用户名输入框中输入 admin,在密码输入框中输入 password,点击登录按钮"
|
||||
|
||||
# 步骤2: 确认进入首页
|
||||
- action: wait
|
||||
duration: 3000
|
||||
|
||||
# 步骤3: 深度探索后台功能
|
||||
- action: explore
|
||||
config:
|
||||
max_depth: 10
|
||||
max_clicks: 500
|
||||
# 引导 AI 关注您截图中显示的菜单
|
||||
focus_patterns: ["管理", "项目", "方案", "审核", "系统"]
|
||||
dangerous_patterns: ["退出", "注销", "删除"]
|
||||
auto_handle_login: false # 已经在第一步处理过了
|
||||
|
||||
# 验证规则 - 基础验证
|
||||
verifications:
|
||||
- type: url_not_contains
|
||||
value: "login"
|
||||
- type: text_contains
|
||||
value: "分析概览"
|
||||
85
tests/quick_test.py
Normal file
85
tests/quick_test.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
快速测试工具 - 最简单的使用方式
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
from tests.universal_tester import TestConfig, UniversalWebTester
|
||||
|
||||
|
||||
def quick_test():
|
||||
"""快速测试函数"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="快速 Web 测试")
|
||||
parser.add_argument("url", help="要测试的网站URL")
|
||||
parser.add_argument("--model", "-m", default="claude",
|
||||
choices=["claude", "openai", "glm", "mimo"],
|
||||
help="AI模型 (默认: claude)")
|
||||
parser.add_argument("--headless", action="store_true",
|
||||
help="无头模式运行")
|
||||
parser.add_argument("--max-clicks", type=int, default=50,
|
||||
help="最大点击次数 (默认: 50)")
|
||||
parser.add_argument("--login", action="store_true",
|
||||
help="是否需要登录")
|
||||
parser.add_argument("--username", help="登录用户名")
|
||||
parser.add_argument("--password", help="登录密码")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建配置
|
||||
config = TestConfig(
|
||||
url=args.url,
|
||||
name=f"快速测试_{args.url}",
|
||||
mode="explore",
|
||||
model=args.model,
|
||||
headless=args.headless,
|
||||
explore_config={
|
||||
"max_depth": 10,
|
||||
"max_clicks": args.max_clicks,
|
||||
"focus_patterns": [],
|
||||
"dangerous_patterns": ["删除", "delete", "退出", "exit", "注销", "logout", "sign out"]
|
||||
}
|
||||
)
|
||||
|
||||
# 添加登录配置
|
||||
if args.login and args.username and args.password:
|
||||
config.login = {
|
||||
"username": args.username,
|
||||
"password": args.password
|
||||
}
|
||||
|
||||
print(f"\n🚀 开始测试: {args.url}")
|
||||
print(f"📊 模式: 探索模式")
|
||||
print(f"🤖 AI模型: {args.model}")
|
||||
print(f"🖥️ 无头模式: {'是' if args.headless else '否'}")
|
||||
print(f"🖱️ 最大点击: {args.max_clicks}")
|
||||
print("="*50)
|
||||
|
||||
# 运行测试
|
||||
tester = UniversalWebTester(config)
|
||||
result = tester.run()
|
||||
|
||||
# 输出结果
|
||||
print("\n" + "="*50)
|
||||
print(f"✅ 测试完成!")
|
||||
print(f"📊 状态: {'通过' if result['status'] == 'passed' else '失败'}")
|
||||
|
||||
if result['steps']:
|
||||
explore_step = result['steps'][0]
|
||||
if explore_step.get('action') == 'explore':
|
||||
explore_result = explore_step.get('result', {})
|
||||
print(f"🖱️ 点击次数: {explore_result.get('click_count', 0)}")
|
||||
print(f"🔍 发现元素: {explore_result.get('discovered_elements', 0)}")
|
||||
print(f"🐛 发现问题: {explore_result.get('bugs_found', 0)}")
|
||||
|
||||
if result['errors']:
|
||||
print("\n❌ 错误信息:")
|
||||
for error in result['errors']:
|
||||
print(f" • {error}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
quick_test()
|
||||
205
tests/smart_test.py
Normal file
205
tests/smart_test.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
增强的零配置测试 - 支持无AI模式
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
from src import WebTester
|
||||
import time
|
||||
|
||||
def smart_test(url: str, model: str = "glm", headless: bool = False, auto_login: bool = True):
|
||||
"""
|
||||
智能测试,即使AI失败也能继续
|
||||
"""
|
||||
|
||||
print("=" * 60)
|
||||
print("🤖 增强智能测试")
|
||||
print("=" * 60)
|
||||
print(f"🌐 目标URL: {url}")
|
||||
print(f"🤖 AI模型: {model}")
|
||||
print(f"🖥️ 无头模式: {'是' if headless else '否'}")
|
||||
print(f"🔐 自动登录: {'是' if auto_login else '否'}")
|
||||
print("-" * 60)
|
||||
|
||||
tester = WebTester(model=model, headless=headless)
|
||||
|
||||
try:
|
||||
# 启动并导航
|
||||
tester.start()
|
||||
tester.goto(url)
|
||||
time.sleep(2) # 等待页面加载
|
||||
|
||||
# 步骤1: 处理登录
|
||||
current_url = tester.browser.page.url
|
||||
print(f"\n📍 当前URL: {current_url}")
|
||||
|
||||
# 检查是否需要登录
|
||||
need_login = ("login" in current_url.lower() or
|
||||
tester.browser.page.locator("input[type='password']").count() > 0)
|
||||
|
||||
if need_login and auto_login:
|
||||
print("\n🔐 检测到登录页面,尝试登录...")
|
||||
|
||||
# 方法1: 尝试AI登录(如果API可用)
|
||||
login_success = False
|
||||
try:
|
||||
result = tester.test("输入用户名admin和密码password,点击登录按钮")
|
||||
time.sleep(3)
|
||||
new_url = tester.browser.page.url
|
||||
if new_url != current_url and "login" not in new_url.lower():
|
||||
print(" ✅ AI登录成功")
|
||||
login_success = True
|
||||
except Exception as e:
|
||||
print(f" ⚠️ AI登录失败: {str(e)[:50]}...")
|
||||
|
||||
# 方法2: 如果AI失败,使用DOM直接登录
|
||||
if not login_success:
|
||||
print(" 🔄 尝试直接登录...")
|
||||
try:
|
||||
# 查找用户名和密码输入框
|
||||
page = tester.browser.page
|
||||
|
||||
# 尝试多种可能的用户名输入框
|
||||
username_selectors = [
|
||||
"input[placeholder*='用户名']",
|
||||
"input[placeholder*='账号']",
|
||||
"input[name='username']",
|
||||
"input[name='account']",
|
||||
"input[type='text']"
|
||||
]
|
||||
|
||||
username_input = None
|
||||
for selector in username_selectors:
|
||||
if page.locator(selector).count() > 0:
|
||||
username_input = page.locator(selector).first
|
||||
break
|
||||
|
||||
# 尝试多种可能的密码输入框
|
||||
password_selectors = [
|
||||
"input[type='password']",
|
||||
"input[placeholder*='密码']",
|
||||
"input[name='password']"
|
||||
]
|
||||
|
||||
password_input = None
|
||||
for selector in password_selectors:
|
||||
if page.locator(selector).count() > 0:
|
||||
password_input = page.locator(selector).first
|
||||
break
|
||||
|
||||
# 填写并提交
|
||||
if username_input and password_input:
|
||||
username_input.fill("admin")
|
||||
password_input.fill("password")
|
||||
|
||||
# 查找登录按钮
|
||||
login_selectors = [
|
||||
"button:has-text('登录')",
|
||||
"button:has-text('登陆')",
|
||||
"button:has-text('确定')",
|
||||
"input[type='submit']",
|
||||
".login-btn"
|
||||
]
|
||||
|
||||
for selector in login_selectors:
|
||||
if page.locator(selector).count() > 0:
|
||||
page.locator(selector).first.click()
|
||||
break
|
||||
|
||||
time.sleep(3)
|
||||
new_url = tester.browser.page.url
|
||||
if new_url != current_url:
|
||||
print(" ✅ 直接登录成功")
|
||||
login_success = True
|
||||
except Exception as e:
|
||||
print(f" ❌ 直接登录失败: {e}")
|
||||
|
||||
if not login_success:
|
||||
print(" ⚠️ 无法自动登录,将测试登录页面功能")
|
||||
|
||||
# 步骤2: 开始探索
|
||||
print("\n🔍 开始智能探索...")
|
||||
|
||||
# 根据是否登录成功调整探索策略
|
||||
if login_success:
|
||||
explore_config = {
|
||||
"max_depth": 5,
|
||||
"max_clicks": 100,
|
||||
"focus_patterns": ["管理", "查询", "新增", "详情"],
|
||||
"dangerous_patterns": ["删除", "退出", "注销"]
|
||||
}
|
||||
else:
|
||||
explore_config = {
|
||||
"max_depth": 2,
|
||||
"max_clicks": 20,
|
||||
"focus_patterns": ["登录", "用户", "密码"],
|
||||
"dangerous_patterns": []
|
||||
}
|
||||
|
||||
# 尝试AI探索,如果失败则使用DOM探索
|
||||
try:
|
||||
explore_result = tester.explore(explore_config)
|
||||
print(" ✅ AI探索完成")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ AI探索失败: {str(e)[:50]}...")
|
||||
print(" 🔄 使用基础探索...")
|
||||
|
||||
# 基础探索:点击所有可见的按钮和链接
|
||||
page = tester.browser.page
|
||||
clickable_elements = page.locator("button, a, [role='button'], input[type='button']")
|
||||
|
||||
clicked_count = 0
|
||||
for i in range(min(clickable_elements.count(), 10)):
|
||||
try:
|
||||
element = clickable_elements.nth(i)
|
||||
if element.is_visible():
|
||||
text = element.inner_text()[:20]
|
||||
print(f" 点击: {text or '无文本'}")
|
||||
element.click()
|
||||
time.sleep(1)
|
||||
page.go_back()
|
||||
time.sleep(1)
|
||||
clicked_count += 1
|
||||
except:
|
||||
continue
|
||||
|
||||
print(f" 📊 基础探索完成,点击了 {clicked_count} 个元素")
|
||||
|
||||
# 步骤3: 生成测试报告
|
||||
print("\n📊 测试总结:")
|
||||
print(f" ✅ 测试完成")
|
||||
print(f" 📄 已访问页面: {tester.browser.page.url}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
tester.stop()
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="增强智能测试工具")
|
||||
parser.add_argument("url", help="要测试的网站URL")
|
||||
parser.add_argument("--model", default="glm",
|
||||
choices=["claude", "openai", "glm", "mimo"],
|
||||
help="AI模型")
|
||||
parser.add_argument("--headless", action="store_true",
|
||||
help="无头模式")
|
||||
parser.add_argument("--no-login", action="store_true",
|
||||
help="跳过自动登录")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 运行测试
|
||||
success = smart_test(args.url, args.model, args.headless, not args.no_login)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,6 +9,233 @@ from src import WebTester
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import List, Dict, Any
|
||||
import time
|
||||
import re
|
||||
|
||||
|
||||
def _ensure_sidebar_open(tester: WebTester) -> None:
|
||||
try:
|
||||
page = tester.browser.page
|
||||
if not page:
|
||||
return
|
||||
|
||||
# 检查是否已有侧边栏文本可见
|
||||
markers = ("立项论证管理", "产品方案管理", "基础数据")
|
||||
visible = False
|
||||
for marker in markers:
|
||||
try:
|
||||
# 检查是否至少有一个 marker 在页面上可见
|
||||
loc = page.get_by_text(marker, exact=False)
|
||||
if loc.count() > 0 and any(loc.nth(i).is_visible() for i in range(loc.count())):
|
||||
visible = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if visible:
|
||||
return
|
||||
|
||||
print(" 📂 侧边栏未发现,尝试点击菜单切换按钮...")
|
||||
# 尝试点击左上角的常见切换图标位置,或者特定的菜单按钮
|
||||
toggle_selectors = [
|
||||
".anticon-menu-fold", ".anticon-menu-unfold",
|
||||
".el-icon-menu", ".toggle-sidebar",
|
||||
".ant-layout-sider-trigger"
|
||||
]
|
||||
|
||||
found_toggle = False
|
||||
for sel in toggle_selectors:
|
||||
try:
|
||||
btn = page.locator(sel)
|
||||
if btn.count() > 0 and btn.first.is_visible():
|
||||
btn.first.click()
|
||||
found_toggle = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not found_toggle:
|
||||
# 记录尝试点击坐标
|
||||
tester.browser.click_at(30, 30)
|
||||
tester.browser.wait(500)
|
||||
tester.browser.click_at(80, 25) # 顶部面包屑左侧
|
||||
|
||||
tester.browser.wait(1500)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def _is_logged_in(tester: WebTester) -> bool:
|
||||
try:
|
||||
page = tester.browser.page
|
||||
if not page:
|
||||
return False
|
||||
url = (page.url or "").lower()
|
||||
if "#/auth/login" in url or "/auth/login" in url or "login" in url:
|
||||
return False
|
||||
|
||||
# DOM marker: if login form is present, consider not logged-in even if URL doesn't include login
|
||||
try:
|
||||
if page.locator("input[type='password']").count() > 0:
|
||||
if page.get_by_text("登录", exact=False).count() > 0:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for marker in ("分析概览", "待办事项", "系统管理"):
|
||||
try:
|
||||
if page.get_by_text(marker, exact=False).count() > 0:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# If we can't find dashboard markers, fall back to a weaker heuristic
|
||||
try:
|
||||
if page.locator("input[type='password']").count() > 0:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _do_login_dom(tester: WebTester, username: str = "admin", password: str = "password") -> bool:
|
||||
page = tester.browser.page
|
||||
if not page:
|
||||
return False
|
||||
|
||||
try:
|
||||
user_locators = [
|
||||
"input[placeholder*='用户']",
|
||||
"input[placeholder*='账号']",
|
||||
"input[name*='user' i]",
|
||||
"input[name*='account' i]",
|
||||
]
|
||||
pwd_locators = [
|
||||
"input[type='password']",
|
||||
"input[placeholder*='密码']",
|
||||
]
|
||||
|
||||
user_el = None
|
||||
for sel in user_locators:
|
||||
loc = page.locator(sel)
|
||||
if loc.count() > 0:
|
||||
user_el = loc.first
|
||||
break
|
||||
if user_el is None:
|
||||
# fall back to the first visible text input
|
||||
loc = page.locator("input[type='text'], input:not([type])")
|
||||
if loc.count() > 0:
|
||||
user_el = loc.first
|
||||
|
||||
pwd_el = None
|
||||
for sel in pwd_locators:
|
||||
loc = page.locator(sel)
|
||||
if loc.count() > 0:
|
||||
pwd_el = loc.first
|
||||
break
|
||||
|
||||
if user_el is None or pwd_el is None:
|
||||
return False
|
||||
|
||||
user_el.fill(username)
|
||||
pwd_el.fill(password)
|
||||
|
||||
clicked = False
|
||||
for text in ("登录", "登 录"):
|
||||
try:
|
||||
btn = page.get_by_role("button", name=re.compile(text))
|
||||
if btn.count() > 0:
|
||||
btn.first.click()
|
||||
clicked = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
btn = page.get_by_text(text, exact=False)
|
||||
if btn.count() > 0:
|
||||
btn.first.click()
|
||||
clicked = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not clicked:
|
||||
return False
|
||||
|
||||
tester.browser.wait_for_load_state("networkidle", timeout=15000)
|
||||
for _ in range(20):
|
||||
if _is_logged_in(tester):
|
||||
return True
|
||||
tester.browser.wait(500)
|
||||
return _is_logged_in(tester)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_logged_in(tester: WebTester, login_goal: str = "填入账号admin 密码password,登录成功") -> bool:
|
||||
if _is_logged_in(tester):
|
||||
return True
|
||||
|
||||
if _do_login_dom(tester):
|
||||
return True
|
||||
|
||||
try:
|
||||
tester.test(login_goal)
|
||||
tester.browser.wait(1500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return _is_logged_in(tester)
|
||||
|
||||
|
||||
def _run_hybrid_steps(tester: WebTester, steps: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
step_results: List[Dict[str, Any]] = []
|
||||
for idx, step in enumerate(steps, 1):
|
||||
action = step.get("action")
|
||||
if action == "goal":
|
||||
_ensure_sidebar_open(tester)
|
||||
before_url = tester.browser.page.url
|
||||
r = tester.test(step.get("goal", ""))
|
||||
after_url = tester.browser.page.url
|
||||
step_results.append({
|
||||
"step": idx,
|
||||
"action": action,
|
||||
"goal": step.get("goal", ""),
|
||||
"result": r,
|
||||
"url_changed": before_url != after_url
|
||||
})
|
||||
|
||||
all_passed = all(x.get("success", False) for x in r.get("results", []))
|
||||
|
||||
# 记录警告
|
||||
if not after_url or before_url == after_url:
|
||||
if any(kw in step.get("goal", "") for kw in ("点击菜单", "跳转", "进入", "研制方案", "项目")):
|
||||
print(f" ⚠️ 警告: 执行 '{step.get('goal', '')}' 后 URL 似乎没有变化")
|
||||
|
||||
if not all_passed:
|
||||
return {"passed": False, "step_results": step_results}
|
||||
elif action == "explore":
|
||||
_ensure_sidebar_open(tester)
|
||||
try:
|
||||
r = tester.explore(step.get("config", {}))
|
||||
step_results.append({"step": idx, "action": action, "result": r})
|
||||
# 如果探索过程中没有任何操作成功,或者发生了严重错误,可以考虑判定为失败
|
||||
if not r.get("action_log") and not r.get("discovered_elements"):
|
||||
return {"passed": False, "step_results": step_results, "error": "探索未发现任何元素或未执行任何操作"}
|
||||
except Exception as e:
|
||||
step_results.append({"step": idx, "action": action, "error": str(e)})
|
||||
return {"passed": False, "step_results": step_results, "error": str(e)}
|
||||
elif action == "wait":
|
||||
duration = step.get("duration", 1000)
|
||||
tester.browser.wait(duration)
|
||||
step_results.append({"step": idx, "action": action, "duration": duration, "result": {"success": True}})
|
||||
else:
|
||||
step_results.append({"step": idx, "action": action, "result": {"success": False, "error": f"unknown action: {action}"}})
|
||||
return {"passed": False, "step_results": step_results}
|
||||
|
||||
return {"passed": True, "step_results": step_results}
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -16,29 +243,66 @@ import time
|
||||
# ============================================================
|
||||
|
||||
TEST_CASES = [
|
||||
# 目标模式: 执行指定目标
|
||||
{
|
||||
"name": "登录",
|
||||
"url": "http://47.99.105.253:8084",
|
||||
"mode": "goal", # 目标驱动模式
|
||||
"goal": "填入账号admin 密码password,登录成功",
|
||||
},
|
||||
|
||||
# 探索模式: AI 自主发现功能
|
||||
{
|
||||
"name": "功能探索",
|
||||
"name": "登录后深度探索",
|
||||
"url": "http://47.99.105.253:8084",
|
||||
"mode": "explore", # 探索模式
|
||||
"mode": "explore",
|
||||
"config": {
|
||||
"max_depth": 3,
|
||||
"max_clicks": 30,
|
||||
"dangerous_patterns": ["删除", "移除", "退出", "注销"], # 记录但不执行
|
||||
"require_login": { # 需要先登录
|
||||
"max_depth": 20, # 探索深度
|
||||
"max_clicks": 2000, # 总点击次数
|
||||
"require_login": { # 探索前的先决条件:登录
|
||||
"goal": "填入账号admin 密码password,登录成功"
|
||||
}
|
||||
},
|
||||
"focus_patterns": ["管理", "设置", "新增"], # 引导 AI 重点测试这些关键词
|
||||
"dangerous_patterns": ["删除", "退出", "注销"] # 发现但不点击,防止测试中断
|
||||
}
|
||||
},
|
||||
|
||||
# 业务流程测试: 技术协议评审完整流程
|
||||
{
|
||||
"name": "技术协议评审流程测试",
|
||||
"url": "http://47.99.105.253:8084",
|
||||
"mode": "hybrid",
|
||||
"steps": [
|
||||
{"action": "goal", "goal": "点击立项论证管理菜单"},
|
||||
{"action": "goal", "goal": "点击项目输入子菜单"},
|
||||
{"action": "goal", "goal": "点击1技术协议及科研合同评审记录"},
|
||||
{"action": "goal", "goal": "点击新增按钮"},
|
||||
# 切换到智能探索模式完成表单填写和提交
|
||||
{"action": "explore", "config": {"max_clicks": 30, "max_depth": 1}},
|
||||
{"action": "wait", "duration": 2000},
|
||||
{"action": "goal", "goal": "点击提交按钮"},
|
||||
{"action": "wait", "duration": 2000},
|
||||
{"action": "goal", "goal": "点击待办事项"},
|
||||
{"action": "goal", "goal": "点击最新提交的个人待办记录"},
|
||||
{"action": "goal", "goal": "点击审核/处理按钮"},
|
||||
{"action": "explore", "config": {"max_clicks": 15, "max_depth": 1}}, # 智能处理审核表单
|
||||
]
|
||||
},
|
||||
|
||||
# 业务流程测试: 产品方案管理流程
|
||||
{
|
||||
"name": "产品方案管理流程测试",
|
||||
"url": "http://47.99.105.253:8084",
|
||||
"mode": "hybrid",
|
||||
"steps": [
|
||||
{"action": "goal", "goal": "点击一级菜单'产品方案管理'"},
|
||||
{"action": "wait", "duration": 1000},
|
||||
{"action": "goal", "goal": "展开二级分类'研制方案'"},
|
||||
{"action": "wait", "duration": 1000},
|
||||
{"action": "goal", "goal": "点击菜单项'研制方案'"},
|
||||
{"action": "wait", "duration": 1000},
|
||||
{"action": "goal", "goal": "点击新增按钮"},
|
||||
# 使用探索模式完成表单填写和提交
|
||||
{"action": "explore", "config": {"max_clicks": 20, "max_depth": 1}},
|
||||
{"action": "wait", "duration": 2000},
|
||||
{"action": "goal", "goal": "点击待办事项"},
|
||||
{"action": "goal", "goal": "点击审批详情"},
|
||||
{"action": "explore", "config": {"max_clicks": 10, "max_depth": 1}},
|
||||
]
|
||||
},
|
||||
|
||||
# 混合模式: 先执行目标,再探索
|
||||
# {
|
||||
# "name": "登录后探索",
|
||||
@@ -73,6 +337,44 @@ def run_single_case(case: Dict[str, Any], model: str = "claude",
|
||||
try:
|
||||
with WebTester(model=model, headless=headless) as tester:
|
||||
tester.goto(url)
|
||||
|
||||
# Always attempt deterministic login first (single-case mode should be robust)
|
||||
_ensure_logged_in(tester)
|
||||
|
||||
# 检查是否需要登录(适用于所有模式)
|
||||
login_needed = False
|
||||
login_goal = None
|
||||
|
||||
# 检查配置中是否需要登录
|
||||
if mode == "explore":
|
||||
config = case.get("config", {})
|
||||
require_login = config.get("require_login")
|
||||
if require_login:
|
||||
login_goal = require_login.get("goal", "")
|
||||
login_needed = True
|
||||
elif mode == "hybrid":
|
||||
# 检查步骤中是否包含登录
|
||||
for step in case.get("steps", []):
|
||||
if "登录" in step.get("goal", "") or "admin" in step.get("goal", ""):
|
||||
login_needed = False # 步骤中已包含登录
|
||||
break
|
||||
else:
|
||||
# 如果步骤中没有登录,但系统可能需要登录
|
||||
login_goal = "填入账号admin 密码password,登录成功"
|
||||
login_needed = True
|
||||
elif mode == "goal":
|
||||
# 目标模式检查是否需要登录
|
||||
goal = case.get("goal", "")
|
||||
if "登录" not in goal and "admin" not in goal:
|
||||
login_goal = "填入账号admin 密码password,登录成功"
|
||||
login_needed = True
|
||||
|
||||
# 执行登录
|
||||
if login_needed and login_goal:
|
||||
if not _is_logged_in(tester):
|
||||
_ensure_logged_in(tester, login_goal)
|
||||
|
||||
_ensure_sidebar_open(tester)
|
||||
|
||||
if mode == "goal":
|
||||
# 目标模式
|
||||
@@ -103,12 +405,9 @@ def run_single_case(case: Dict[str, Any], model: str = "claude",
|
||||
|
||||
elif mode == "hybrid":
|
||||
# 混合模式
|
||||
for step in case.get("steps", []):
|
||||
if step.get("action") == "goal":
|
||||
tester.test(step["goal"])
|
||||
elif step.get("action") == "explore":
|
||||
tester.explore(step.get("config", {}))
|
||||
result["status"] = "passed"
|
||||
hybrid_res = _run_hybrid_steps(tester, case.get("steps", []))
|
||||
result["status"] = "passed" if hybrid_res.get("passed") else "failed"
|
||||
result["hybrid"] = hybrid_res
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
@@ -116,24 +415,70 @@ def run_single_case(case: Dict[str, Any], model: str = "claude",
|
||||
return result
|
||||
|
||||
|
||||
def run_tests(model: str = "claude", headless: bool = False):
|
||||
def run_tests(model: str = "claude", headless: bool = False, cases: List[Dict[str, Any]] = None):
|
||||
"""串行运行所有测试用例"""
|
||||
results = []
|
||||
|
||||
selected_cases = cases if cases is not None else TEST_CASES
|
||||
|
||||
with WebTester(model=model, headless=headless) as tester:
|
||||
for i, case in enumerate(TEST_CASES, 1):
|
||||
for i, case in enumerate(selected_cases, 1):
|
||||
name = case.get("name", f"Test {i}")
|
||||
url = case["url"]
|
||||
mode = case.get("mode", "goal")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🧪 [{i}/{len(TEST_CASES)}] {name}")
|
||||
print(f"🧪 [{i}/{len(selected_cases)}] {name}")
|
||||
print(f" URL: {url}")
|
||||
print(f" Mode: {mode}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
try:
|
||||
tester.goto(url)
|
||||
|
||||
_ensure_logged_in(tester)
|
||||
|
||||
# 检查是否需要登录(适用于所有模式)
|
||||
login_needed = False
|
||||
login_goal = None
|
||||
|
||||
# 检查配置中是否需要登录
|
||||
if mode == "explore":
|
||||
config = case.get("config", {}).copy()
|
||||
require_login = config.pop("require_login", None)
|
||||
if require_login:
|
||||
login_goal = require_login.get("goal", "")
|
||||
login_needed = True
|
||||
elif mode == "hybrid":
|
||||
# 检查步骤中是否包含登录
|
||||
for step in case.get("steps", []):
|
||||
if "登录" in step.get("goal", "") or "admin" in step.get("goal", ""):
|
||||
login_needed = False # 步骤中已包含登录
|
||||
break
|
||||
else:
|
||||
# 如果步骤中没有登录,但系统可能需要登录
|
||||
login_goal = "填入账号admin 密码password,登录成功"
|
||||
login_needed = True
|
||||
elif mode == "goal":
|
||||
# 目标模式检查是否需要登录
|
||||
goal = case.get("goal", "")
|
||||
if "登录" not in goal and "admin" not in goal:
|
||||
login_goal = "填入账号admin 密码password,登录成功"
|
||||
login_needed = True
|
||||
|
||||
# 执行登录
|
||||
if login_needed and login_goal:
|
||||
print(f" 🔎 检查登录状态...")
|
||||
if not _is_logged_in(tester):
|
||||
ok = _ensure_logged_in(tester, login_goal)
|
||||
if ok:
|
||||
print(f" ✅ 登录成功")
|
||||
else:
|
||||
print(f" ⚠️ 登录未确认成功,继续执行")
|
||||
else:
|
||||
print(f" ✅ 已处于登录状态,跳过登录步骤")
|
||||
|
||||
_ensure_sidebar_open(tester)
|
||||
|
||||
if mode == "goal":
|
||||
goal = case.get("goal", "")
|
||||
@@ -148,6 +493,12 @@ def run_tests(model: str = "claude", headless: bool = False):
|
||||
status = "passed"
|
||||
else:
|
||||
print(f"⚠️ 部分失败: {failed_count}/{result['steps']} 步骤失败")
|
||||
first_failed = next((r for r in result.get("results", []) if not r.get("success", False)), None)
|
||||
if first_failed:
|
||||
action_type = first_failed.get("action_type") or first_failed.get("action") or "unknown"
|
||||
target = first_failed.get("target") or first_failed.get("element") or first_failed.get("description") or ""
|
||||
err = first_failed.get("error") or first_failed.get("reason") or ""
|
||||
print(f" ❌ 首个失败: {action_type} {target} {err}".strip())
|
||||
status = "failed"
|
||||
|
||||
print(f"📄 报告: {result['report']}")
|
||||
@@ -161,33 +512,101 @@ def run_tests(model: str = "claude", headless: bool = False):
|
||||
|
||||
elif mode == "explore":
|
||||
config = case.get("config", {}).copy()
|
||||
login_status = "skipped"
|
||||
login_error = None
|
||||
|
||||
# 如果需要先登录
|
||||
require_login = config.pop("require_login", None)
|
||||
if require_login:
|
||||
login_goal = require_login.get("goal", "")
|
||||
if login_goal:
|
||||
print(f" 🔐 执行登录...")
|
||||
tester.test(login_goal)
|
||||
tester.browser.wait(1000)
|
||||
try:
|
||||
# 检查是否已登录
|
||||
print(f" 🔎 检查登录状态...")
|
||||
if _is_logged_in(tester):
|
||||
print(f" ✅ 已处于登录状态,跳过登录步骤")
|
||||
login_status = "already_logged_in"
|
||||
else:
|
||||
print(f" 🔐 执行登录: {login_goal}")
|
||||
login_result = tester.test(login_goal)
|
||||
tester.browser.wait(3000)
|
||||
|
||||
# 检查登录结果
|
||||
login_success = all(r.get("success", False) for r in login_result.get("results", []))
|
||||
if login_success:
|
||||
login_status = "success"
|
||||
else:
|
||||
login_status = "partial_failure"
|
||||
# 登录部分失败,但仍继续探索
|
||||
print(f" ⚠️ 登录步骤部分失败,继续尝试探索...")
|
||||
|
||||
# 等待页面加载
|
||||
print(f" 🔎 确认登录跳转结果...")
|
||||
tester.browser.wait_for_load_state("networkidle")
|
||||
tester.browser.wait(2000)
|
||||
except Exception as e:
|
||||
login_error = str(e)
|
||||
login_status = "error"
|
||||
print(f" ⚠️ 登录过程出错: {e},继续尝试探索...")
|
||||
|
||||
# 执行探索
|
||||
# 无论登录结果如何,都执行探索
|
||||
print(f" 🔍 开始功能探索...")
|
||||
result = tester.explore(config)
|
||||
|
||||
elements = len(result.get("discovered_elements", []))
|
||||
bugs = len(result.get("bug_list", []))
|
||||
|
||||
print(f"✅ 探索完成: 发现 {elements} 个元素, {bugs} 个问题")
|
||||
print(f"📄 报告: {result.get('report', '')}")
|
||||
|
||||
results.append({
|
||||
"name": name,
|
||||
"status": "passed",
|
||||
"elements": elements,
|
||||
"bugs": bugs,
|
||||
"report": result.get("report", ""),
|
||||
})
|
||||
try:
|
||||
result = tester.explore(config)
|
||||
|
||||
elements = len(result.get("discovered_elements", []))
|
||||
bugs = len(result.get("bug_list", []))
|
||||
|
||||
print(f"✅ 探索完成: 发现 {elements} 个元素, {bugs} 个问题")
|
||||
print(f"📄 报告: {result.get('report', '')}")
|
||||
|
||||
# 根据登录状态决定最终状态
|
||||
final_status = "passed" if login_status in ("success", "already_logged_in", "skipped") else "partial"
|
||||
|
||||
results.append({
|
||||
"name": name,
|
||||
"status": final_status,
|
||||
"login_status": login_status,
|
||||
"login_error": login_error,
|
||||
"elements": elements,
|
||||
"bugs": bugs,
|
||||
"report": result.get("report", ""),
|
||||
})
|
||||
except Exception as e:
|
||||
print(f" ❌ 探索过程出错: {e}")
|
||||
results.append({
|
||||
"name": name,
|
||||
"status": "failed",
|
||||
"login_status": login_status,
|
||||
"error": str(e),
|
||||
})
|
||||
|
||||
elif mode == "hybrid":
|
||||
try:
|
||||
hybrid_res = _run_hybrid_steps(tester, case.get("steps", []))
|
||||
status = "passed" if hybrid_res.get("passed") else "failed"
|
||||
if status != "passed":
|
||||
step_results = hybrid_res.get("step_results", [])
|
||||
last = step_results[-1] if step_results else None
|
||||
if last:
|
||||
step_idx = last.get("step")
|
||||
action = last.get("action")
|
||||
goal_txt = last.get("goal", "")
|
||||
print(f" ❌ Hybrid 失败在 step {step_idx}: {action} {goal_txt}".strip())
|
||||
results.append({
|
||||
"name": name,
|
||||
"status": status,
|
||||
"mode": "hybrid",
|
||||
"hybrid": hybrid_res,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"❌ 失败: {e}")
|
||||
results.append({
|
||||
"name": name,
|
||||
"status": "failed",
|
||||
"mode": "hybrid",
|
||||
"error": str(e),
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 失败: {e}")
|
||||
@@ -201,7 +620,7 @@ def run_tests(model: str = "claude", headless: bool = False):
|
||||
return results
|
||||
|
||||
|
||||
def run_tests_parallel(model: str = "claude", max_workers: int = 3):
|
||||
def run_tests_parallel(model: str = "claude", max_workers: int = 3, cases: List[Dict[str, Any]] = None):
|
||||
"""
|
||||
并行运行所有测试用例
|
||||
|
||||
@@ -210,17 +629,15 @@ def run_tests_parallel(model: str = "claude", max_workers: int = 3):
|
||||
max_workers: 最大并行数(默认 3)
|
||||
"""
|
||||
print(f"\n🚀 并行模式启动 (workers={max_workers})")
|
||||
print(f"📋 待执行测试: {len(TEST_CASES)} 个\n")
|
||||
selected_cases = cases if cases is not None else TEST_CASES
|
||||
print(f"📋 待执行测试: {len(selected_cases)} 个\n")
|
||||
|
||||
results = []
|
||||
start_time = time.time()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
# 提交所有任务
|
||||
future_to_case = {
|
||||
executor.submit(run_single_case, case, model, True): case
|
||||
for case in TEST_CASES
|
||||
}
|
||||
future_to_case = {executor.submit(run_single_case, case, model, True): case for case in selected_cases}
|
||||
|
||||
# 收集结果
|
||||
for future in as_completed(future_to_case):
|
||||
@@ -251,21 +668,57 @@ def _print_summary(results: List[Dict[str, Any]]):
|
||||
print("📊 测试总结")
|
||||
print(f"{'='*60}")
|
||||
passed = sum(1 for r in results if r["status"] == "passed")
|
||||
failed = len(results) - passed
|
||||
partial = sum(1 for r in results if r["status"] == "partial")
|
||||
failed = sum(1 for r in results if r["status"] == "failed")
|
||||
|
||||
print(f"✅ 通过: {passed}")
|
||||
if partial > 0:
|
||||
print(f"⚠️ 部分通过: {partial}")
|
||||
print(f"❌ 失败: {failed}")
|
||||
if results:
|
||||
print(f"📈 通过率: {passed/len(results)*100:.1f}%")
|
||||
# 通过率计算将 partial 视为 0.5
|
||||
rate = (passed + partial * 0.5) / len(results) * 100
|
||||
print(f"📈 通过率: {rate:.1f}%")
|
||||
|
||||
failed_cases = [r for r in results if r.get("status") == "failed"]
|
||||
if failed_cases:
|
||||
print("\n❌ 失败用例:")
|
||||
for r in failed_cases:
|
||||
name = r.get("name", "")
|
||||
report = r.get("report", "")
|
||||
err = r.get("error", "")
|
||||
line = f"- {name}"
|
||||
if report:
|
||||
line += f" | {report}"
|
||||
if err:
|
||||
line += f" | {err}"
|
||||
print(line)
|
||||
|
||||
|
||||
def run_single_test(url: str, goal: str, model: str = "claude"):
|
||||
def run_single_test(url: str, goal: str, model: str = "claude", headless: bool = False):
|
||||
"""运行单个测试"""
|
||||
with WebTester(model=model) as tester:
|
||||
tester.goto(url)
|
||||
result = tester.test(goal)
|
||||
print(f"✅ 完成: {result['steps']} 步骤")
|
||||
case = {"name": goal[:30], "url": url, "mode": "goal", "goal": goal}
|
||||
result = run_single_case(case, model=model, headless=headless)
|
||||
if result.get("status") == "passed":
|
||||
print(f"✅ 完成")
|
||||
else:
|
||||
print(f"❌ 失败: {result.get('error', 'unknown')}")
|
||||
if result.get("report"):
|
||||
print(f"📄 报告: {result['report']}")
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
def _select_cases(args) -> List[Dict[str, Any]]:
|
||||
cases: List[Dict[str, Any]] = TEST_CASES
|
||||
if getattr(args, "index", None) is not None:
|
||||
idx = int(args.index)
|
||||
if idx < 1 or idx > len(TEST_CASES):
|
||||
raise ValueError(f"index out of range: {idx}")
|
||||
cases = [TEST_CASES[idx - 1]]
|
||||
if getattr(args, "case", None):
|
||||
q = str(args.case).strip().lower()
|
||||
cases = [c for c in cases if q in str(c.get("name", "")).lower()]
|
||||
return cases
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -278,16 +731,26 @@ if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="AI Web Tester - 测试用例运行器")
|
||||
parser.add_argument("--url", help="单个测试的 URL")
|
||||
parser.add_argument("--goal", help="单个测试的目标描述")
|
||||
parser.add_argument("--model", default="claude", choices=["claude", "openai"], help="AI 模型")
|
||||
parser.add_argument("--model", default="mimo", choices=["claude", "openai", "mimo", "glm"], help="AI 模型")
|
||||
parser.add_argument("--headless", action="store_true", help="无头模式运行")
|
||||
parser.add_argument("--parallel", action="store_true", help="并行执行测试")
|
||||
parser.add_argument("--workers", type=int, default=3, help="并行工作线程数")
|
||||
parser.add_argument("--list", action="store_true", help="列出内置测试用例")
|
||||
parser.add_argument("--case", help="按用例名子串筛选(大小写不敏感)")
|
||||
parser.add_argument("--index", type=int, help="按序号选择用例(从 1 开始,基于内置用例列表)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list:
|
||||
for i, c in enumerate(TEST_CASES, 1):
|
||||
print(f"[{i}] {c.get('name', '')} ({c.get('mode', 'goal')})")
|
||||
sys.exit(0)
|
||||
|
||||
selected_cases = _select_cases(args)
|
||||
|
||||
if args.url and args.goal:
|
||||
run_single_test(args.url, args.goal, args.model)
|
||||
run_single_test(args.url, args.goal, args.model, args.headless)
|
||||
elif args.parallel:
|
||||
run_tests_parallel(model=args.model, max_workers=args.workers)
|
||||
run_tests_parallel(model=args.model, max_workers=args.workers, cases=selected_cases)
|
||||
else:
|
||||
run_tests(model=args.model, headless=args.headless)
|
||||
run_tests(model=args.model, headless=args.headless, cases=selected_cases)
|
||||
|
||||
354
tests/universal_tester.py
Normal file
354
tests/universal_tester.py
Normal file
@@ -0,0 +1,354 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user