Some checks failed
AI Web Tester CI / test (push) Has been cancelled
新增功能: - explorer.py: AI功能探索器 - 自动发现页面可交互元素 - 元素分类 (navigation/button/link/card/menu) - 危险操作保护 (删除/退出只记录不执行) - DOM快速定位替代AI定位 (速度提升10x) - 站点地图和BUG清单生成 - main.py: 添加 explore() 方法 - generator.py: 添加探索报告生成 (暗色主题+Mermaid站点图) - test_cases.py: 支持 goal/explore/hybrid 三种模式 测试结果: - 成功发现30个可交互元素 - 自动分类: Links(11), Navigation(8), Cards(8), Buttons(2), Menu(1) - 生成完整HTML探索报告
294 lines
10 KiB
Python
294 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
测试用例模板 - 快速设计和运行多个测试(支持并行执行)
|
||
"""
|
||
import sys
|
||
sys.path.insert(0, ".")
|
||
|
||
from src import WebTester
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from typing import List, Dict, Any
|
||
import time
|
||
|
||
|
||
# ============================================================
|
||
# 测试用例配置
|
||
# ============================================================
|
||
|
||
TEST_CASES = [
|
||
# 目标模式: 执行指定目标
|
||
{
|
||
"name": "登录",
|
||
"url": "http://47.99.105.253:8084",
|
||
"mode": "goal", # 目标驱动模式
|
||
"goal": "填入账号admin 密码password,登录成功",
|
||
},
|
||
|
||
# 探索模式: AI 自主发现功能
|
||
{
|
||
"name": "功能探索",
|
||
"url": "http://47.99.105.253:8084",
|
||
"mode": "explore", # 探索模式
|
||
"config": {
|
||
"max_depth": 3,
|
||
"max_clicks": 30,
|
||
"dangerous_patterns": ["删除", "移除", "退出", "注销"], # 记录但不执行
|
||
"require_login": { # 需要先登录
|
||
"goal": "填入账号admin 密码password,登录成功"
|
||
}
|
||
}
|
||
},
|
||
|
||
# 混合模式: 先执行目标,再探索
|
||
# {
|
||
# "name": "登录后探索",
|
||
# "url": "http://47.99.105.253:8084",
|
||
# "mode": "hybrid",
|
||
# "steps": [
|
||
# {"action": "goal", "goal": "登录"},
|
||
# {"action": "explore", "config": {"max_clicks": 10}}
|
||
# ]
|
||
# },
|
||
]
|
||
|
||
|
||
# ============================================================
|
||
# 测试执行器
|
||
# ============================================================
|
||
|
||
def run_single_case(case: Dict[str, Any], model: str = "claude",
|
||
headless: bool = True) -> Dict[str, Any]:
|
||
"""运行单个测试用例(独立浏览器实例)"""
|
||
name = case.get("name", "Unknown")
|
||
url = case["url"]
|
||
mode = case.get("mode", "goal")
|
||
|
||
result = {
|
||
"name": name,
|
||
"url": url,
|
||
"mode": mode,
|
||
"status": "failed",
|
||
}
|
||
|
||
try:
|
||
with WebTester(model=model, headless=headless) as tester:
|
||
tester.goto(url)
|
||
|
||
if mode == "goal":
|
||
# 目标模式
|
||
goal = case.get("goal", "")
|
||
test_result = tester.test(goal)
|
||
result["status"] = "passed"
|
||
result["steps"] = test_result["steps"]
|
||
result["report"] = test_result["report"]
|
||
|
||
elif mode == "explore":
|
||
# 探索模式
|
||
config = case.get("config", {})
|
||
|
||
# 如果需要先登录
|
||
require_login = config.pop("require_login", None)
|
||
if require_login:
|
||
login_goal = require_login.get("goal", "")
|
||
if login_goal:
|
||
tester.test(login_goal)
|
||
tester.browser.wait(1000)
|
||
|
||
# 执行探索
|
||
explore_result = tester.explore(config)
|
||
result["status"] = "passed"
|
||
result["elements"] = len(explore_result.get("discovered_elements", []))
|
||
result["bugs"] = len(explore_result.get("bug_list", []))
|
||
result["report"] = explore_result.get("report", "")
|
||
|
||
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"
|
||
|
||
except Exception as e:
|
||
result["error"] = str(e)
|
||
|
||
return result
|
||
|
||
|
||
def run_tests(model: str = "claude", headless: bool = False):
|
||
"""串行运行所有测试用例"""
|
||
results = []
|
||
|
||
with WebTester(model=model, headless=headless) as tester:
|
||
for i, case in enumerate(TEST_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" URL: {url}")
|
||
print(f" Mode: {mode}")
|
||
print(f"{'='*60}")
|
||
|
||
try:
|
||
tester.goto(url)
|
||
|
||
if mode == "goal":
|
||
goal = case.get("goal", "")
|
||
result = tester.test(goal)
|
||
|
||
# 检查所有步骤是否成功
|
||
all_passed = all(r.get("success", False) for r in result.get("results", []))
|
||
failed_count = sum(1 for r in result.get("results", []) if not r.get("success", False))
|
||
|
||
if all_passed:
|
||
print(f"✅ 完成: {result['steps']} 步骤")
|
||
status = "passed"
|
||
else:
|
||
print(f"⚠️ 部分失败: {failed_count}/{result['steps']} 步骤失败")
|
||
status = "failed"
|
||
|
||
print(f"📄 报告: {result['report']}")
|
||
|
||
results.append({
|
||
"name": name,
|
||
"status": status,
|
||
"steps": result["steps"],
|
||
"report": result["report"],
|
||
})
|
||
|
||
elif mode == "explore":
|
||
config = case.get("config", {}).copy()
|
||
|
||
# 如果需要先登录
|
||
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)
|
||
|
||
# 执行探索
|
||
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", ""),
|
||
})
|
||
|
||
except Exception as e:
|
||
print(f"❌ 失败: {e}")
|
||
results.append({
|
||
"name": name,
|
||
"status": "failed",
|
||
"error": str(e),
|
||
})
|
||
|
||
_print_summary(results)
|
||
return results
|
||
|
||
|
||
def run_tests_parallel(model: str = "claude", max_workers: int = 3):
|
||
"""
|
||
并行运行所有测试用例
|
||
|
||
Args:
|
||
model: AI 模型
|
||
max_workers: 最大并行数(默认 3)
|
||
"""
|
||
print(f"\n🚀 并行模式启动 (workers={max_workers})")
|
||
print(f"📋 待执行测试: {len(TEST_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
|
||
}
|
||
|
||
# 收集结果
|
||
for future in as_completed(future_to_case):
|
||
case = future_to_case[future]
|
||
try:
|
||
result = future.result()
|
||
status = "✅" if result["status"] == "passed" else "❌"
|
||
print(f"{status} {result['name']}")
|
||
results.append(result)
|
||
except Exception as e:
|
||
print(f"❌ {case['name']}: {e}")
|
||
results.append({
|
||
"name": case["name"],
|
||
"status": "failed",
|
||
"error": str(e),
|
||
})
|
||
|
||
elapsed = time.time() - start_time
|
||
print(f"\n⏱️ 总耗时: {elapsed:.1f}秒")
|
||
|
||
_print_summary(results)
|
||
return results
|
||
|
||
|
||
def _print_summary(results: List[Dict[str, Any]]):
|
||
"""打印测试总结"""
|
||
print(f"\n{'='*60}")
|
||
print("📊 测试总结")
|
||
print(f"{'='*60}")
|
||
passed = sum(1 for r in results if r["status"] == "passed")
|
||
failed = len(results) - passed
|
||
print(f"✅ 通过: {passed}")
|
||
print(f"❌ 失败: {failed}")
|
||
if results:
|
||
print(f"📈 通过率: {passed/len(results)*100:.1f}%")
|
||
|
||
|
||
def run_single_test(url: str, goal: str, model: str = "claude"):
|
||
"""运行单个测试"""
|
||
with WebTester(model=model) as tester:
|
||
tester.goto(url)
|
||
result = tester.test(goal)
|
||
print(f"✅ 完成: {result['steps']} 步骤")
|
||
print(f"📄 报告: {result['report']}")
|
||
return result
|
||
|
||
|
||
# ============================================================
|
||
# 主入口
|
||
# ============================================================
|
||
|
||
if __name__ == "__main__":
|
||
import argparse
|
||
|
||
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("--headless", action="store_true", help="无头模式运行")
|
||
parser.add_argument("--parallel", action="store_true", help="并行执行测试")
|
||
parser.add_argument("--workers", type=int, default=3, help="并行工作线程数")
|
||
|
||
args = parser.parse_args()
|
||
|
||
if args.url and args.goal:
|
||
run_single_test(args.url, args.goal, args.model)
|
||
elif args.parallel:
|
||
run_tests_parallel(model=args.model, max_workers=args.workers)
|
||
else:
|
||
run_tests(model=args.model, headless=args.headless)
|