结构
本文介绍如何使用Python实现需求转为playwright测试脚本,本文的思路是:
读取需求->生成测试脚本->是够通过—是—>测试结束 ^ | |————否—————本程序的代码结果如下
|——logs| || ———test_run.log(运行后自动生成)|——outputs| || ——— /test_history (目录、运行后自动生成)| test_current.py(运行后生成的测试程序)| fix_report.json(运行后自动生成)||——skills| ||———/test-orchestrator| ||——————skill.md: 能力文件| /scripts| ||—————————executor.py :运行测试程序| fixer.py:修复测试程序| generator.py:生成测试程序| orchestrator.py:调度者|——main.py 主程序|——req.txt 需求文档实现程序
main.py
主测试程序python
#!/usr/bin/env python# -*- coding: utf-8 -*-"""一键启动测试自动化系统"""import sysfrom pathlib import Path# 添加脚本路径sys.path.insert(0, str(Path(__file__).parent / "skills" / "test-orchestrator" / "scripts"))from orchestrator import TestOrchestratordef main(): orchestrator = TestOrchestrator( req_file="req.txt", max_retry=5, headless=True ) success = orchestrator.run() if success: print("\n测试自动化完成,所有用例通过!") print(f" 最终测试文件: outputs/test_current.py") else: print("\n达到最大重试次数,请人工介入检查") print(f" 修复报告: outputs/fix_report.json") return 0 if success else 1if __name__ == "__main__": sys.exit(main())executor.py
测试执行器python
#!/usr/bin/env python"""测试执行器 - 运行 pytest 并解析结果"""import subprocessimport jsonimport refrom pathlib import Pathfrom typing import Dict, Listclass TestExecutor: def __init__(self, headless: bool = True): self.headless = headless def run(self, test_file: str) -> Dict: """ 执行测试文件,返回结构化结果 """ test_path = Path(test_file) if not test_path.exists(): return { "success": False, "error": f"测试文件不存在: {test_file}", "exit_code": -1, "passed": [], "failed": [] } # 构建命令 cmd = ["pytest", str(test_path), "-v", "--tb=short", "--color=no"] # 修复:headed 是 flag,不加 =value if not self.headless: cmd.append("--headed") # 添加 JSON 报告 report_file = test_path.parent / ".pytest_report.json" cmd.extend(["--json-report", f"--json-report-file={report_file}"]) try: import time start = time.time() result = subprocess.run( cmd, capture_output=True, text=True, timeout=120, env={**subprocess.os.environ, "PYTHONUNBUFFERED": "1"} ) duration = time.time() - start # 解析结果 passed = [] failed = [] for line in result.stdout.split("\n"): if "PASSED" in line and "::" in line: match = re.search(r'(\w+)::(\w+)', line) if match: passed.append(match.group(2)) elif "FAILED" in line and "::" in line: match = re.search(r'(\w+)::(\w+)', line) if match: failed.append(match.group(2)) error_msg = "" if report_file.exists(): try: with open(report_file) as f: report = json.load(f) for test in report.get("tests", []): if test.get("outcome") == "failed": error_msg += test.get("longrepr", "")[:1000] except: pass if not error_msg: error_msg = result.stdout + "\n" + result.stderr return { "success": result.returncode == 0, "passed": passed, "failed": failed, "error": error_msg[:2000], "exit_code": result.returncode, "duration": duration } except subprocess.TimeoutExpired: return { "success": False, "error": "测试执行超时(超过 120 秒)", "exit_code": -2, "passed": [], "failed": [] } except Exception as e: return { "success": False, "error": f"执行异常: {str(e)}", "exit_code": -3, "passed": [], "failed": [] }fixer.py
修复测试程序python
import osimport reimport loggingfrom typing import Dict, Optional, List, Tuplefrom openai import OpenAIimport ast logger = logging.getLogger(__name__)class TestFixer: def __init__(self): self.client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) self.model = os.getenv("QWEN_MODEL", "qwen-plus") self.max_retries = 3 def _classify_error(self, error_msg: str, traceback: str = "") -> Dict: """精细化错误分类""" error_lower = error_msg.lower() traceback_lower = traceback.lower() # Playwright 特有错误 if "timeout" in error_lower: if "expect" in error_lower: return {"type": "AssertionTimeout", "severity": "high"} return {"type": "NavigationTimeout", "severity": "medium"} elif "locator" in error_lower and "not found" in error_lower: return {"type": "ElementNotFound", "severity": "high"} elif "not visible" in error_lower or "hidden" in error_lower: return {"type": "ElementNotVisible", "severity": "medium"} elif "assertion" in error_lower or "expected" in error_lower: # 提取实际值和期望值 expected = re.search(r"expected:?\s*['\"](.+?)['\"]", error_msg, re.I) actual = re.search(r"actual:?\s*['\"](.+?)['\"]", error_msg, re.I) return { "type": "AssertionError", "severity": "high", "expected": expected.group(1) if expected else None, "actual": actual.group(1) if actual else None } elif "database" in traceback_lower or "mysql" in traceback_lower: return {"type": "DatabaseError", "severity": "critical"} else: return {"type": "UnknownError", "severity": "low"} def _extract_code_block(self, content: str) -> Optional[str]: """提取代码块,支持多种格式""" # 尝试提取 ```python ... ``` 代码块 # 修复了正则表达式中的引号转义问题 patterns = [ r"```python\n(.*?)```", r"```\n(.*?)```", "```python\n(.*?)```", "```\n(.*?)```" ] for pattern in patterns: match = re.search(pattern, content, re.DOTALL) if match: return match.group(1).strip() # 如果没有代码块标记,尝试提取Python代码 if "def test_" in content or "class Test" in content: return content.strip() return None def _generate_fix_prompt(self, code: str, error_info: Dict, requirements: str, html_context: str = "", db_context: str = "") -> str: """生成修复提示词""" error_detail = error_info.get("error", "无详细信息") traceback = error_info.get("traceback", "") error_analysis = self._classify_error(error_detail, traceback) prompt = f""" 你是一个 Playwright + Pytest 自动化测试专家。请分析错误并修复测试代码。 ## 错误分析 - 错误类型: {error_analysis['type']} - 严重程度: {error_analysis['severity']} - 错误信息: {error_detail} ## 原始需求 {requirements} ## 失败的测试代码 ```python {code} # 完整错误堆栈 {traceback or error_detail} ``` """ # 添加HTML上下文(如果有) if html_context: truncated_html = html_context[:2000] if len(html_context) > 2000 else html_context prompt += f""" ## 页面HTML结构(调试信息) ```html {truncated_html} ``` """ # 添加数据库上下文(如果有) if db_context: truncated_db = db_context[:2000] if len(db_context) > 2000 else db_context prompt += f""" ## 数据库状态 {truncated_db} """ prompt += """ ## 修复要求 1. 标点符号保护:错误消息必须与需求文档完全一致,保留所有标点符号(,。!) 2. 等待策略:添加合适的等待(wait_for_selector, wait_for_timeout) 3. 错误处理:添加 try-except 和重试机制 4. 数据库清理:确保测试数据正确清理 5. 断言优化:使用 expect() 而不是 assert ## 常见问题修复指南 - 超时错误:增加 timeout 参数,添加等待条件 - 元素未找到:检查选择器,添加等待,确保页面已加载 - 断言失败:检查实际值格式(如换行符、空格),使用 to_contain_text 而不是 to_have_text - 数据库错误:检查外键约束,确保测试数据完整 请输出修复后的完整 Python 代码,只返回代码,不要解释。 """ return prompt def fix(self, code: str, error_info: Dict, requirements: str, html_context: str = "", db_context: str = "") -> str: """ 修复测试代码 Args: code: 原始测试代码 error_info: 错误信息 {'error': '错误消息', 'traceback': '堆栈信息'} requirements: 产品需求文档 html_context: 页面HTML上下文(可选) db_context: 数据库状态上下文(可选) """ for attempt in range(self.max_retries): try: prompt = self._generate_fix_prompt( code, error_info, requirements, html_context, db_context ) response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=4000 ) content = response.choices[0].message.content fixed_code = self._extract_code_block(content) if not fixed_code: logger.warning(f"尝试 {attempt + 1}: 无法提取代码块,使用原始内容") fixed_code = content.strip() # 验证修复后的代码基本结构 if self._validate_code(fixed_code): logger.info(f"修复成功 (尝试 {attempt + 1}/{self.max_retries})") logger.info(f"代码长度: {len(fixed_code)} 字符") return fixed_code else: logger.warning(f"尝试 {attempt + 1}: 代码验证失败,重试...") except Exception as e: logger.error(f"修复尝试 {attempt + 1} 失败: {e}") if attempt == self.max_retries - 1: logger.error("达到最大重试次数,返回原始代码") return code return code def _validate_code(self, code: str) -> bool: """验证修复后的代码基本结构""" required_elements = [ "def test", "self.page", "expect(" ] # 修复逻辑:只有在发现缺失元素时才返回 False,否则循环结束后返回 True for element in required_elements: if element not in code: logger.warning(f"代码缺少必要元素: {element}") return False # 只有在缺失时才返回 False return True # 所有元素都存在,返回 Truegenerator.py
产生测试程序python
#!/usr/bin/env python# -*- coding: utf-8 -*-"""测试代码生成器 - 调用 LLM 从需求生成 Playwright + Pytest 代码"""import osimport sysimport jsonimport reimport loggingfrom typing import List, Dictfrom openai import OpenAIlogger = logging.getLogger(__name__)class TestGenerator: def __init__(self): self.client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) self.model = os.getenv("QWEN_MODEL", "qwen-plus") def _sanitize_text(self, text: str) -> str: """清理文本中的特殊字符,但保留标点符号""" if not text: return text # 只移除表情符号和特殊 Unicode 字符,保留中文标点保留的标点:,。!?;:、""''()【】 cleaned = re.sub(r'[^\u4e00-\u9fff\u3400-\u4dbf\u0000-\u007f\u0080-\u00ff\u3000-\u303f\uff00-\uffef]', '', text) return cleaned def _extract_code(self, content: str) -> str: """从 LLM 返回内容中提取代码""" content = content.strip() # 查找代码块 if "```python" in content: start = content.find("```python") + 9 end = content.find("```", start) if end != -1: return content[start:end].strip() elif "```" in content: parts = content.split("```") if len(parts) >= 2: code = parts[1].strip() # 移除语言标识 lines = code.split("\n") if lines and lines[0].lower() in ["python", "py"]: code = "\n".join(lines[1:]).lstrip() return code return content def generate(self, requirements: str) -> str: """让 LLM 直接生成 Playwright 测试代码 - 使用参数化形式""" # 清理输入 requirements = self._sanitize_text(requirements) # 强调使用参数化的 Prompt prompt = f""" 你是一个 Playwright + Pytest 自动化测试专家。请根据以下产品需求,直接生成可执行的端到端测试代码。 ## 产品需求 {requirements} ## 核心要求 ## 标点符号保护规则(最高优先级 - 违反将导致测试失败) 【严重警告】你必须严格遵守以下规则,否则测试代码将无法通过: 1. 绝对保留所有标点符号:逗号(,)、句号(。)、感叹号(!)、问号(?)、分号(;)、冒号(:)、顿号(、) 2. 严禁删除、替换或省略任何标点符号 3. 错误消息必须与需求文档中的文本完全一致,包括标点符号 【正确示例 - 必须这样做】: ERROR_MESSAGES = {{ "CONTACT_NOT_FOUND": "您输入的手机号或Email不存在,请重新输入!", "VERICODE_ERROR": "验证码错误,请重新输入!", "PASSWORD_USED_BEFORE": "这个密码以前设置过,请用一个新密码!", }} 【错误示例 - 绝对禁止这样做】: ERROR_MESSAGES = {{ "CONTACT_NOT_FOUND": "您输入的手机号或Email不存在请重新输入", # 错误!缺少逗号和感叹号 "VERICODE_ERROR": "验证码错误请重新输入", # 错误!缺少逗号和感叹号 "PASSWORD_USED_BEFORE": "这个密码以前设置过请用一个新密码", # 错误!缺少逗号和感叹号 }} 4. 所有断言中使用的错误消息字符串必须包含完整标点符号 5. 生成 ERROR_MESSAGES 字典时,必须逐字复制需求文档中的错误消息,包括标点 - 分析需求:理解需求中的功能点、业务流程、验证规则、边界条件 - 设计测试用例:覆盖正常流程、异常流程、边界条件 - 使用参数化:必须使用 `from parameterized import parameterized` 和 `@parameterized.expand` - 生成代码:为相似场景使用参数化,不同场景创建独立测试函数 - 所有测试方法必须放在测试类中 - 不测试字段为空的情形 ## 技术规范 - 使用 Playwright 同步 API:`from playwright.sync_api import Page, expect` - 使用 pytest 框架 - 使用 `@pytest.fixture(autouse=True)` 在每个测试前后执行数据库清理 - 使用 `@pytest.mark.usefixtures("db_cleanup")` 应用到测试类 - 选择器优先级: * `page.get_by_role("button", name="按钮文字")` - 按角色定位 * `page.get_by_label("字段标签")` - 按标签定位 * `page.get_by_placeholder("提示文字")` - 按占位符定位 * `page.locator("#id")` - 按 ID 定位 * `page.locator(".class")` - 按class定位 - URL 地址:根据需求中的描述推断,默认使用 `http://localhost:3000` - 等待策略:优先使用 `expect(...).to_be_visible()` 而非固定延时 - 对于反向测试用例,一次经允许出现一处错误的数据 - 判断URL请使用语句:assert self.page.url.startswith(REGISTER_URL) ## 代码模板 ``` import pytest from playwright.sync_api import Page, expect import pymysql DB_CONFIG = {{ 'host': 'localhost', 'user': 'root', 'password': '123456', 'database': 'chatgptebusiness' }} REGISTER_URL = "http://127.0.0.1:8080/ChatGPTEbusiness/jsp/RegisterPage.jsp" MESSAGES = {{ "LOGIN_PAGE": "登录页面", "INVALID_USERNAME": "账号必须是5-20位的字母或数字", "INVALID_PASSWORD": "密码必须包含大小写字母数字和特殊字符,长度在5-30之间", "INVALID_REPASSWORD": "密码确认不一致", "INVALID_PHONE": "请输入有效的中国手机号", "INVALID_EMAIL": "请输入有效的Email地址", "USERNAME_CONFLICT": "注册用户的用户名必须唯一", "PHONE_CONFLICT": "注册用户的手机必须唯一", "EMAIL_CONFLICT": "注册用户的邮箱必须唯一", }} class TestUserRegistration: @pytest.fixture(autouse=True) def setup_and_teardown(self, page: Page): \"\"\"每个测试前后执行数据库清理,并注入page\"\"\" self.page = page self.db = pymysql.connect(**DB_CONFIG) self._clear_tables() yield self.db.close() def _clear_tables(self): \"\"\"清空数据库表确保测试环境干净\"\"\" try: with self.db.cursor() as cursor: cursor.execute("SET FOREIGN_KEY_CHECKS = 0") cursor.execute("DELETE FROM user") cursor.execute("SET FOREIGN_KEY_CHECKS = 1") self.db.commit() except Exception as e: pytest.fail(f"数据库表清除失败: {{str(e)}}") def _fill_registration_form(self, username: str, password: str, repassword: str, phone: str, email: str): \"\"\"填写注册表单并点击注册按钮 - 使用正确的定位器\"\"\" # 等待页面完全加载 self.page.wait_for_load_state("networkidle") # 使用 ID 定位器 self.page.locator("#username").fill(username) self.page.locator("#password").fill(password) self.page.locator("#confirmPassword").fill(repassword) self.page.locator("#phone").fill(phone) self.page.locator("#email").fill(email) # 点击注册按钮 self.page.locator("button[type='submit']").click() @pytest.mark.parametrize("username,password,repassword,phone,email,expected_message", [ # GTC-001: Valid registration ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", MESSAGES["LOGIN_PAGE"]), # GTC-002: Username too short ("abc", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", MESSAGES["INVALID_USERNAME"]), ... # GTC-007: Password too long ("jerrygu", "A" * 31 + "Zxcv123@", "A" * 31 + "Zxcv123@", "13687654321", "a@126.com", MESSAGES["INVALID_PASSWORD"]), ... ("jerrygu", "Zxcv123@", "Zxcv123@", "1368765432", "a@126.com", MESSAGES["INVALID_PHONE"]), ... ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a126@com", MESSAGES["INVALID_EMAIL"]), ... ]) def test_registration_validation(self, username, password, repassword, phone, email, expected_message): \"\"\"测试注册表单各字段验证规则参数化\"\"\" self.page.goto(REGISTER_URL) # 等待页面加载 self.page.wait_for_load_state("networkidle") # 填写表单 self._fill_registration_form(username, password, repassword, phone, email) # 根据预期结果进行断言 if expected_message == MESSAGES["LOGIN_PAGE"]: # 成功注册,验证跳转到登录页 expect(self.page).to_have_title(MESSAGES["LOGIN_PAGE"], timeout=10000) # 验证用户已写入数据库 with self.db.cursor() as cursor: cursor.execute("SELECT COUNT(*) FROM user WHERE username = %s", (username,)) count = cursor.fetchone()[0] assert count == 1, f"Expected 1 user with username '{{username}}' but found {{count}}" else: # 等待错误信息出现 # 验证错误提示 if "账号必须是" in expected_message: error_locator = self.page.locator("#usernameError") elif "密码必须包含" in expected_message: error_locator = self.page.locator("#passwordError") elif "密码确认不一致" in expected_message: error_locator = self.page.locator("#confirmPasswordError") elif "请输入有效的中国手机号" in expected_message: error_locator = self.page.locator("#phoneError") elif "请输入有效的Email地址" in expected_message: error_locator = page.locator("#emailError") else: error_locator = self.page.locator("#registerError") expect(error_locator).to_contain_text(expected_message, timeout=5000) # 验证停留在注册页面 assert self.page.url.startswith(REGISTER_URL) @pytest.mark.parametrize("username,password,repassword,phone,email,dup_username,dup_phone,dup_email,expected_message", [ # GTC-021: Duplicate username ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", "jerrygu", "18767896543", "b@126.com", MESSAGES["USERNAME_CONFLICT"]), # GTC-022: Duplicate phone ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", "peter", "13687654321", "c@126.com", MESSAGES["PHONE_CONFLICT"]), # GTC-023: Duplicate email ("jerrygu", "Zxcv123@", "Zxcv123@", "13687654321", "a@126.com", "peter", "18767896543", "a@126.com", MESSAGES["EMAIL_CONFLICT"]), ]) def test_duplicate_constraint(self, username, password, repassword, phone, email, dup_username, dup_phone, dup_email, expected_message): \"\"\"测试唯一性约束:用户名、手机号、邮箱重复注册\"\"\" # 第一次注册 - 应该成功 self.page.goto(REGISTER_URL) self.page.wait_for_load_state("networkidle") self._fill_registration_form(username, password, repassword, phone, email) expect(self.page).to_have_title(MESSAGES["LOGIN_PAGE"], timeout=10000) # 第二次注册 - 应该失败并显示冲突错误 self.page.goto(REGISTER_URL) self.page.wait_for_load_state("networkidle") self._fill_registration_form(dup_username, password, repassword, dup_phone, dup_email) # 检查注册错误信息 expect(self.page.locator("#registerError")).to_contain_text(expected_message, timeout=5000) assert self.page.url.startswith(REGISTER_URL) # 验证没有创建重复用户 with self.db.cursor() as cursor: cursor.execute("SELECT COUNT(*) FROM user WHERE username = %s OR phone = %s OR email = %s", (dup_username, dup_phone, dup_email)) count = cursor.fetchone()[0] assert count == 1, f"重复注册意外插入了用户,找到 {{count}} 条记录" def test_password_confirmation_mismatch_after_typing(self): \"\"\"测试密码确认框实时校验输入不一致时显示错误\"\"\" ... ``` **重要提醒** - 测试类必须使用 @pytest.mark.usefixtures("db_cleanup") 装饰器 - 测试类名使用 PascalCase,如 TestUserLogin、TestRegistrationPage - 测试方法名使用 snake_case,如 test_valid_login、test_password_validation - 如果测试方法不在类中使用参数化,则 page: Page 是唯一参数 **输出要求** - 只返回 Python 代码,不要任何解释 - 代码必须可以直接执行 - 测试函数命名使用英文,清晰描述测试场景 - 添加必要的注释说明关键步骤 - 不要使用任何表情符号或特殊 Unicode 字符 - 优先使用 @pytest.mark.parametrize 处理多组测试数据 - 请生成测试代码 """ response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.0, max_tokens=8000 ) content = response.choices[0].message.content content = self._sanitize_text(content) #提取代码 code = self._extract_code(content) #确保有必要的导入 if "from parameterized import parameterized" not in code: #在导入部分添加 parameterized if "from playwright.sync_api" in code: code = code.replace( "from playwright.sync_api import Page, expect", "from playwright.sync_api import Page, expect\nfrom parameterized import parameterized" ) else: code = "import re\nimport pytest\nfrom playwright.sync_api import Page, expect\nfrom parameterized import parameterized\n\n" + code return code def fix(self, code: str, error_info: Dict, requirements: str) -> str: """修复错误的测试代码 - 保持参数化形式""" #清理输入 requirements = self._sanitize_text(requirements) code = self._sanitize_text(code) error_msg = error_info.get('error', '无详细信息') prompt = f""" 你是一个 Playwright + Pytest 自动化测试专家。下面的测试代码运行失败了,请分析错误并修复。 **原始产品需求** {requirements} **当前测试代码(有错误)** ``` {code} ``` **实际运行错误** ``` {error_msg} ``` 请根据错误信息输出修复后的完整 Python 代码,只返回代码。 """ try: response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=4000 ) content = response.choices[0].message.content content = self._sanitize_text(content) fixed_code = self._extract_code(content) #确保修复后的代码也包含 parameterized 导入 if "from parameterized import parameterized" not in fixed_code: if "from playwright.sync_api" in fixed_code: fixed_code = fixed_code.replace( "from playwright.sync_api import Page, expect", "from playwright.sync_api import Page, expect\nfrom parameterized import parameterized" ) else: fixed_code = "from parameterized import parameterized\n" + fixed_code return fixed_code except Exception as e: logger.error(f"修复代码时出错: {e}") return self._generate_fallback(requirements) def _generate_fallback(self, requirements: str) -> str: """生成降级模板 - 使用参数化形式""" requirements = self._sanitize_text(requirements) return f''' """ 自动生成测试模板 - 请根据以下需求手动完善 需求摘要:{requirements} **使用说明: ** 使用 @pytest.mark.parametrize 装饰器组织测试数据 根据需求中的功能点设计测试用例 使用 Playwright 定位器选择页面元素 添加适当的断言验证 """ import re import pytest from playwright.sync_api import Page, expect from parameterized import parameterized 定义测试数据 TEST_DATA = [ ("正常流程", "valid_input", "expected_success"), ("异常流程", "invalid_input", "expected_error"), ] @pytest.mark.parametrize(TEST_DATA) def test_feature_scenarios(page: Page, scenario_name, test_input, expected_output): """参数化测试 - {scenario_name}""" 导航到页面 page.goto("http://localhost:3000/") TODO: 根据需求添加测试步骤 page.get_by_label("字段名").fill(test_input) page.get_by_role("button", name="按钮文字").click() 断言验证 expect(page.get_by_text(expected_output)).to_be_visible() pass '''orchestrator.py
总调度python
#核心编排器#!/usr/bin/env python"""测试编排器 - 实现"生成→运行→修复"闭环"""import osimport sysimport jsonimport timeimport shutilimport loggingimport subprocessfrom pathlib import Pathfrom datetime import datetimefrom typing import Optional, Dict, List, Tupleimport re# 添加项目根目录到路径sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))from generator import TestGeneratorfrom executor import TestExecutorfrom fixer import TestFixer# ========== 日志配置 ==========logging.basicConfig( level=logging.INFO, format='%(asctime)s | %(levelname)-8s | %(message)s', handlers=[ logging.FileHandler('logs/test_run.log'), logging.StreamHandler() ])logger = logging.getLogger(__name__)class TestOrchestrator: """测试编排器 - 协调生成、执行、修复的完整流程""" def __init__( self, req_file: str = "req.txt", max_retry: int = 5, headless: bool = True, output_dir: str = "outputs", llm_config: Optional[Dict] = None, custom_rules: Optional[str] = None ): self.req_file = Path(req_file) self.max_retry = max_retry self.headless = headless self.output_dir = Path(output_dir) self.history_dir = self.output_dir / "test_history" # 保存新增的参数 self.llm_config = llm_config or {} self.custom_rules = custom_rules or "" # 确保目录存在 self.output_dir.mkdir(parents=True, exist_ok=True) self.history_dir.mkdir(parents=True, exist_ok=True) Path("logs").mkdir(exist_ok=True) # 初始化组件 - 传递配置 self.generator = TestGenerator() self.executor = TestExecutor(headless=headless) self.fixer = TestFixer() # 状态记录 self.current_test_file = self.output_dir / "test_current.py" self.fix_report_file = self.output_dir / "fix_report.json" self.fix_history: List[Dict] = [] self.iteration = 0 self.preserve_punctuation = True def read_requirements(self) -> str: """读取需求文件""" if not self.req_file.exists(): raise FileNotFoundError(f"需求文件不存在: {self.req_file}") content = self.req_file.read_text(encoding="utf-8") logger.info(f"已读取需求文件 ({len(content)} 字符)") return content def save_version(self, code: str, iteration: int, status: str): """保存代码版本到历史目录""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") version_file = self.history_dir / f"test_v{iteration:02d}_{status}_{timestamp}.py" version_file.write_text(code, encoding="utf-8") logger.debug(f"已保存版本: {version_file.name}") def run_single_iteration(self, requirements: str, previous_code: Optional[str] = None) -> Tuple[bool, str, Dict]: """ 执行单次迭代:生成/修复 → 运行 → 返回结果 Returns: (success, code, execution_result) """ self.iteration += 1 logger.info(f"\n{'='*50}") logger.info(f"第 {self.iteration} 轮迭代开始") logger.info(f"{'='*50}") # Step 1: 生成或获取代码 if previous_code is None: logger.info("首次生成测试代码...") code = self.generator.generate(requirements) action = "generate" else: logger.info("尝试修复代码...") # 需要上次执行的错误信息 last_result = self.fix_history[-1] if self.fix_history else {} code = self.fixer.fix(previous_code, last_result, requirements) action = "fix" # 保存版本 self.save_version(code, self.iteration, action) # 写入当前测试文件 self.current_test_file.write_text(code, encoding="utf-8") logger.info(f"测试代码已写入: {self.current_test_file}") # Step 2: 执行测试 logger.info("执行测试...") exec_result = self.executor.run(str(self.current_test_file)) # Step 3: 记录结果 record = { "iteration": self.iteration, "action": action, "timestamp": datetime.now().isoformat(), "success": exec_result["success"], "passed_tests": exec_result.get("passed", []), "failed_tests": exec_result.get("failed", []), "error": exec_result.get("error"), "exit_code": exec_result.get("exit_code") } self.fix_history.append(record) # 更新修复报告 self._save_fix_report() # 打印结果 if exec_result["success"]: logger.info(f"第 {self.iteration} 轮测试全部通过!") logger.info(f"通过用例: {exec_result.get('passed', [])}") else: logger.warning(f"第 {self.iteration} 轮测试失败") logger.warning(f"失败用例: {exec_result.get('failed', [])}") if exec_result.get("error"): logger.warning(f" 错误摘要: {exec_result['error'][:200]}...") return exec_result["success"], code, exec_result def _save_fix_report(self): """保存修复过程报告""" report = { "req_file": str(self.req_file), "max_retry": self.max_retry, "final_success": self.fix_history[-1]["success"] if self.fix_history else False, "total_iterations": self.iteration, "history": self.fix_history } self.fix_report_file.write_text( json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8" ) def run(self) -> bool: """ 主流程:持续迭代直到全部通过或达到最大重试 Returns: True 如果最终全部通过,False 如果达到上限仍有失败 """ logger.info("="*60) logger.info("启动测试编排器") logger.info(f"需求文件: {self.req_file}") logger.info(f"最大重试: {self.max_retry}") logger.info(f"无头模式: {self.headless}") logger.info("="*60) # 读取需求 requirements = self.read_requirements() success = False current_code = None last_result = None # 迭代循环 while self.iteration < self.max_retry: success, current_code, last_result = self.run_single_iteration( requirements, current_code ) if success: break # 最终结果处理 logger.info("\n" + "="*60) if success: logger.info(" 所有测试通过!") logger.info(f"最终代码: {self.current_test_file}") logger.info(f"总迭代次数: {self.iteration}") logger.info(f"修复报告: {self.fix_report_file}") return True else: logger.error(f"达到最大重试次数 ({self.max_retry}),仍有测试失败") logger.error(f"失败的用例: {last_result.get('failed', []) if last_result else '未知'}") logger.error(f"请查看修复报告: {self.fix_report_file}") return Falsedef main(): import argparse parser = argparse.ArgumentParser(description="测试编排器 - 自动生成、执行、修复测试") parser.add_argument("--req_file", type=str, default="req.txt") parser.add_argument("--max_retry", type=int, default=5) parser.add_argument("--headless", action="store_true", default=True) parser.add_argument("--no-headless", action="store_false", dest="headless") parser.add_argument("--verbose", action="store_true") args = parser.parse_args() if args.verbose: logging.getLogger().setLevel(logging.DEBUG) orchestrator = TestOrchestrator( req_file=args.req_file, max_retry=args.max_retry, headless=args.headless ) success = orchestrator.run() sys.exit(0 if success else 1)if __name__ == "__main__": main()SKILL.md
markdown
---name: test-orchestratordescription: | 端到端测试自动化编排器。 读取需求 → 生成测试 → 执行 → 失败自动修复 → 重试,直至全部通过。version: 1.0.0author: AI Agent---# 测试编排技能## 能力概述本技能实现了一个闭环的测试自动化流程:1. 从 `req.txt` 解析产品需求2. 调用 LLM 设计测试用例并生成 Playwright + Pytest 代码3. 自动执行生成的测试4. 如果失败,分析错误并调用 LLM 修复代码5. 重复步骤 3-4 直到全部通过或达到最大重试次数(默认 5 次)## 输入参数- `--req_file`: 需求文件路径,默认 `req.txt`- `--max_retry`: 最大修复重试次数,默认 `5`- `--headless`: 是否无头模式运行,默认 `true`- `--verbose`: 详细日志输出## 输出产物- `outputs/test_current.py`: 最终通过的测试代码- `outputs/fix_report.json`: 修复过程记录- `logs/test_run.log`: 完整运行日志## 编排流程┌─────────────┐│ 读取 req.txt │└──────┬──────┘▼┌─────────────┐│ LLM 生成代码 │◄─────────────────┐└──────┬──────┘ │▼ │┌─────────────┐ ││ 执行测试 │ │└──────┬──────┘ │▼ │┌───────┐ 否 ││ 通过? ├────────┐ │└───┬───┘ ▼ ││是 ┌─────────────┐ │▼ │ 分析错误 │ │┌────────┐ │ LLM 修复代码│────┘│ 完成! │ └─────────────┘└────────┘ (最多 5 次)## 退出码- `0`: 所有测试通过- `1`: 达到最大重试次数仍有失败- `2`: 需求文件不存在或格式错误需求文档
注册需求
****基本需求****输入username(#username)、password(#password)、confirmPassword(#confirmPassword)、phone(#phone)、email(#email)单击【注册】按键注册成功,进入登录页面,标题为“登录页面”,URL为:http://127.0.0.1:8080/ChatGPTEbusiness/jsp/LoginPage.jsp****格式要求****- 账号(必填):文本框,长度为5-20位,可以包含大小写英文字符(必填)或数字(选填)。<label for="username">账号 (5-20位字母或数字):</label><input type="text" id="username" name="username" placeholder="输入账号" autocomplete="username" required>- 密码(必填):密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符,密码通过SHA256散列进行传输和存储。<label for="password">密码 (5-30位,包含大小写字母、数字和特殊字符):</label><input type="password" id="password" name="password" placeholder="输入密码" autocomplete="current-password" required>- 确认密码(必填):密码框,确认密码的值必须与密码的值一致。<label for="confirmPassword">密码确认:</label><input type="password" id="confirmPassword" name="confirmPassword" placeholder="输入确认密码" autocomplete="current-password" required>- 手机号(必填):手机框,需符合中国手机号码格式。<label for="phone">手机号 (中国):</label><input type="tel" id="phone" name="phone" placeholder="输入手机号" autocomplete="tel" required>- Email(必填):Email框,需符合国际标准Email格式<label for="email">邮箱:</label><input type="email" id="email" name="email" placeholder="输入邮箱" autocomplete="email" required><label for="email">邮箱:</label>****错误提示****- 输入非法格式的账号,比如长度<5位、长度<20位 、不包含大小写英文字符,提交后报'账号必须是5-20位的字母或数字'错误信息,停留在注册页面<div id="usernameError" class="error"></div>- 输入非法格式的密码,比如长度<5位、长度<30位 、不包含大小写英文字符、数字、特殊字符,提交后报'密码必须包含大小写字母数字和特殊字符,长度在5-30之间'错误信息,停留在注册页面<div id="passwordError" class="error"></div>- 输入合法的密码,但是确认密码与密码不一致,提交后报'密码确认不一致'错误信息,停留在注册页面<div id="confirmPasswordError" class="error"></div>- 输入非法格式的手机号,比如长度不为11的数字、非1开始的数值,电话号中含有大小写英文字符,提交后报'请输入有效的中国手机号'错误信息,停留在注册页面<div id="phoneError" class="error"></div>- 输入非法格式的email,比如a@com邮件(其他错误格式HTML5会自动校验)、提交后报'请输入有效的Email地址'错误信息,停留在注册页面<div id="emailError" class="error"></div>- 注册的用户账号已经存在,报'注册用户的用户名必须唯一'错误信息,停留在注册页面- 注册的手机已经存在,报'注册用户的手机必须唯一'错误信息,停留在注册页面- 注册的Email已经存在,报'注册用户的邮箱必须唯一'错误信息,停留在注册页面<div id="registerError" class="error"></div>- 测试注册按钮在必填字段为空时不被禁用****数据库信息****# 测试环境配置SQL ```DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': '123456', 'database': 'chatgptebusiness'}- user表格式 id int 自增1 username varchar(50) password varchar(100) phone varchar(50) email varchar(50) ```- 请执行每个测试用例前,建立数据库连接,清除user表;执行每个测试用例后,请断开数据库连接- 注册用户成功,请进入数据库中进行检查,注册的数据是否正确存在数据库中****URL****- url=http://127.0.0.1:8080/ChatGPTEbusiness/jsp/RegisterPage.jsp需求包括:
登录需求
****基本需求****- 输入username(#username)、password(#password)单击【登录】按键- 登录成功,进入欢迎页面,标题为“系统欢迎您”,URL为:127.0.0.1:8080/ChatGPTEbusiness/jsp/WelcomePage.jsp****格式要求****- 用户名(必填):文本框,长度为5-20位,必须包含大写或小写英文字符,也可以包含数字,不允许包含除大小写英文字符、数字其他外的字符(/^[a-zA-Z0-9]{5,20}$/)。<label for="username">用户名:</label><input type="text" id="username" name="username" placeholder="输入账号" autocomplete="username" required>- 密码(必填):密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/)。密码通过SHA256散列进行传输和存储。<label for="password">密码:</label><input type="password" id="password" name="password" placeholder="输入密码" autocomplete="current-password" required>****错误提示****- 输入非法格式的账号,比如长度<5位、长度<20位 、不包含大小写英文字符,报'账号必须是5-20位的字母或数字'错误信息,停留在注册页面<div id="usernameError" class="error"></div>- 输入非法格式的密码,比如长度<5位、长度<30位 、不包含大小写英文字符、数字、特殊字符,报'密码必须包含大小写字母数字和特殊字符,长度在5-30之间'错误信息,停留在注册页面- 用户名或密码错误<div id="loginError" class="error"></div>- 登录失败****数据库信息****SQL ```# 测试环境配置DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': '123456', 'database': 'chatgptebusiness'}- user表格式 id int 自增1 username varchar(50) password varchar(100) phone varchar(50) email varchar(50) ```- 请执行每个测试用例前,建立数据库连接,在user表中插入一条user表信息(password通过SHA256散列);执行每个测试用例后,请断开数据库连接****URL****- url=http://127.0.0.1:8080/ChatGPTEbusiness/jsp/LoginPage.jsp注意
找回密码需求
***找回密码产品需求***- 1.进入输入手机号或Email页面- 2.如果手机号或Email格式不正确,前端返回:"请输入有效的中国手机号或Email"- 3.将手机号或Email传入后端,后端验证是否存在这个手机号或Email- 4.如果不存在后端返回:"您输入的手机号或Email不存在,请重新输入!" **注意**不是“您输入的手机号或Email不存在请重新输入”- 5.如果手机号存在,记录这个手机号所属于的用户ID号(uid);如果Email存在,记录这个Email所属于的用户ID号(uid)- 6.生成验证码(6位数字),存储在表code中,值为(id,uid,code),其中id自增一、uid用户外键、code验证码- 7.如果是手机号向用户手机发送验证码;如果是Email向Email发送验证码。- 8.前端进入重置密码页面- 9.用户输入验证码(6位数字)、新密码(长度为5-30位,必须包含大小写英文字符、数字和特殊字符,密码通过SHA256散列进行传输和存储)和新密码确认码。- 10.验证码不符合格式,前端返回:"验证码必须是6位数字"- 11. 新密码不符合格式,前端返回:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"- 12.新密码与新密码确认码,前端返回:"密码确认不一致!"- 13.将验证码、新密码与新密码确认码传入后端- 14. 如果验证码不正确,后端返回提示信息:"验证码错误"。- 15.与password比较,如果新密码以前使用过,后端返回提示信息:"新密码以前使用过,请设置其他密码"。- 16.否则,将旧密码添加至密码历史password表中(id,uid,password)。其中id自增一、uid用户外键、password旧密码。- 17.将新密码更新至当前用户user表中。- 18.删除验证码code表中的临时验证码记录。- 19.设置成功后,重定向至登录页面。***各字段格式要求*******VeriCodePage.jsp****- 手机号码或电子邮箱,格式:手机号码或电子邮箱 <label for="contact">手机号码或电子邮箱:</label> <input type="text" id="contact" name="contact" placeholder="输入手机号码或邮箱" required>- 提交按钮:<button type="submit" id="sendCode">发送验证码</button>- 网页标题:输入手机号码或电子邮箱 验证请用:assert CONTRACT_URL in self.page.url****RecoverPage.jsp****- 验证码:6位数字:/^\d{6}$/ <label for="identifyingCode">验证码:</label> <input type="text" id="identifyingCode" name="identifyingCode" placeholder="输入验证码" maxlength="6" required>- 新密码,密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/) <label for="newPassword">新密码:</label> <input type="password" id="newPassword" name="newPassword" placeholder="输入新密码" required>- 新密码确认码,密码框,长度为5-30位,必须包含大小写英文字符、数字和特殊字符(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/) <label for="confirmPassword">确认新密码:</label> <input type="password" id="confirmPassword" name="confirmPassword" placeholder="确认新密码" required> - 提交按钮 <button type="submit">确定</button>- 建议:定位"输入新密码"与"确认新密码"使用 self.page.locator("#newPassword").fill(password) self.page.locator("#confirmPassword").fill(confirm_password)- 网页标题:找回密码 验证请用:assert VERICODE_URL in self.page.url***断言信息*******VeriCodePage.jsp****- 提交成功,进入重置密码页面,页面"找回密码"- 您输入的手机号或Email不存在,请重新输入!**注意**不是“您输入的手机号或Email不存在请重新输入” <div id="VeriCodeError" class="error"></div>- 不考虑字段为空的情形****RecoverPage.jsp****- 提交成功,进入登录页面,页面"登录页面"- 验证码必须是6位数字 <div id="identifyingCodeError" class="error"></div>- 密码必须包含大小写字母、数字和特殊字符,长度在5-30之间 <div id="newPasswordError" class="error"></div>- 密码确认不一致! <div id="confirmPasswordError" class="error"></div>- 验证码错误,请重新输入!- 这个密码以前设置过,请用一个新密码! <div id="recoverError" class="error"></div>- 不考虑字段为空的情形***数据库信息***``` DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': '123456', 'database': 'chatgptebusiness'}```****user表格式****SQL ``` CREATE TABLE IF NOT EXISTS user( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(50) NOT NULL, email VARCHAR(50) NOT NULL); ``` ****code表格式****SQL ``` CREATE TABLE code( id INT AUTO_INCREMENT PRIMARY KEY, uid INT NOT NULL, code CHAR(6) NOT NULL, FOREIGN KEY(uid) REFERENCES user(id));```****password表格式****SQL ```CREATE TABLE password( id INT AUTO_INCREMENT PRIMARY KEY, uid INT NOT NULL, password VARCHAR(100) NOT NULL, FOREIGN KEY(uid) REFERENCES user(id));```- 发送验证码存入code表- 当输入的新密码验证正确,将旧密码存入password表,更新用户表中密码,删除code表中数据。***URL***- 输入phone或者Email:http://127.0.0.1:8080/ChatGPTEbusiness/jsp/VeriCodePage.jsp- 重置密码:http://127.0.0.1:8080/ChatGPTEbusiness/jsp/RecoverPage.jsp***测试流程*******正确流程1****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的手机信息。- 3,点击【发送验证码】按键。- 4,向数据库code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@123。- 6,点击【确认】按键。- 7,进入登录页面,页面标题为“登录页面”****正确流程2****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的Email信息。- 3,点击【发送验证码】按键。- 4,向数据库code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@123。- 6,点击【确认】按键。- 7,进入登录页面,页面标题为“登录页面”****错误流程1:非有效的手机号和Email字符****- 1,在VeriCodePage.jsp中输入非手机非Email字符,比如jerrygu。- 2,点击【发送验证码】按键。- 3,在<div id="contactError" class="error"></div>显示"请输入有效的中国手机号或Email"****错误流程2:手机号或Email不存在****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入非第1步用户信息的Email和Phone信息。- 3,点击【发送验证码】按键。- 4,在<div id="VeriCodeError" class="error"></div>显示"您输入的手机号或Email不存在,请重新输入!" **注意**不是“您输入的手机号或Email不存在请重新输入”****错误流程3:验证码格式错误****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,输入验证码:A12345、新密码:Zxcv@123、确认新密码:Zxcv@123。- 5,点击【确定】按钮- 6,在<div id="identifyingCodeError" class="error"></div>显示:"验证码必须是6位数字"****错误流程4:验证码不是所生成的****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,输入验证码:123456、新密码:Zxcv@123:Zxcv@123。- 5,点击【确定】按钮- 6,在<div id="recoverError" class="error"></div>显示:"验证码错误,请重新输入!"****错误流程5:密码确认不一致!****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@124。- 6,点击【确定】按钮- 7,在<div id="confirmPasswordError" class="error"></div>显示:"密码确认不一致!"****错误流程6:密码以前设置过****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入上一步生成的验证码、新密码:Zxcv@123、确认新密码:Zxcv@123。- 6,在password表中插入(uid与SHA256后的字符Zxcv@123)。- 7,点击【确定】按钮- 8,在<div id="recoverError" class="error"></div>显示:"这个密码以前设置过,请用一个新密码!"****错误流程7:新密码无特殊字符****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入验证码、新密码:Zxcv123、确认新密码:Zxcv123。- 6,点击【确定】按钮- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"****错误流程8:新密码无大写字符****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入验证码、新密码:zxcv@123、确认新密码:zxcv@123。- 6,点击【确定】按钮- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"****错误流程9:新密码无小写字符****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,向数据库code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入验证码、新密码:ZXCV@123、确认新密码:ZXCV@123。- 6,点击【确定】按钮- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"****错误流程10:新密码无数字字符****- 1,向user表中注册表一条用户信息。- 2,在VeriCodePage.jsp中输入第1步用户信息的邮件信息。- 3,点击【发送验证码】按键。- 4,向数据库表code表插入数据(uid,code) ,其中uid为user ID,code为6位数字的验证码- 5,在RecoverPage.jsp验证码中输入验证码、新密码:ZXCV@、确认新密码:ZXCV@。- 6,点击【确定】按钮- 7,在<div id="newPasswordError" class="error"></div>显示:"密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"注意