feat: 增强测试框架功能
Some checks failed
AI Web Tester CI / test (push) Has been cancelled

主要改进:
- 新增统一测试器 (universal_tester.py) 支持多种测试模式
- 优化测试报告生成器,支持汇总报告和操作截图
- 增强探索器 DFS 算法和状态指纹识别
- 新增智能测试配置 (smart_test.yaml)
- 改进 AI 模型集成 (GLM/Gemini 支持)
- 添加开发调试工具和文档
This commit is contained in:
empty
2026-01-05 20:23:02 +08:00
parent 3447ea340a
commit 1f1cc4db9a
31 changed files with 4631 additions and 770 deletions

188
tests/README.md Normal file
View 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
View 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()

View 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"

View 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"
}
]
}

View 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: ["退出", "注销"] # 只避开危险操作

View 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
View 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
View 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()

View File

@@ -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
View 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()