#!/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()