提交 4faab7e2 authored 作者: 陈泽健's avatar 陈泽健

docs(test): 更新自动化测试用例生成需求文档和问题修复记录

- 添加工具函数模块初始化文件
- 创建自动化测试用例生成需求文档
- 创建计划执行文档详细设计各功能模块
- 记录分类管理与添加按钮定位取值错误问题修复
- 整理XPath语法错误和元素定位策略优化方案
上级 eaa16b43
......@@ -110,7 +110,16 @@
"Bash(del:*)",
"Bash(git --no-pager show HEAD:AuxiliaryTool/ScriptTool/ServiceSelfInspection/modules/ConfigIPCheck.psm1)",
"Bash(python:*)",
"Bash(git checkout:*)"
"Bash(git checkout:*)",
"mcp__chrome-devtools__navigate_page",
"mcp__chrome-devtools__take_snapshot",
"mcp__chrome-devtools__evaluate_script",
"mcp__chrome-devtools__take_screenshot",
"mcp__chrome-devtools__fill",
"mcp__chrome-devtools__click",
"mcp__chrome-devtools__wait_for",
"mcp__chrome-devtools__press_key",
"mcp__chrome-devtools__new_page"
]
}
}
# 自动化测试用例生成工具
通过Claude Code + MCP技术,自动访问系统并生成JSON格式的测试用例。
## 功能特点
- 自动登录系统(支持固定验证码)
- 两级菜单导航
- 智能元素定位(优先ID/Name/Class,备选XPATH)
- 支持添加/编辑/删除操作
- 自动生成JSON测试用例
## 目录结构
```
TestCaseGenerator/
├── main.py # 主程序入口
├── config_manager.py # 配置管理模块
├── browser_operator.py # 浏览器操作模块
├── login_handler.py # 登录处理模块
├── navigation_handler.py # 导航处理模块
├── element_locator.py # 元素定位模块
├── operation_executor.py # 操作执行模块
├── testcase_generator.py # 测试用例生成模块
├── config/ # 配置文件目录
│ ├── system_config.json # 系统配置
│ └── module_config.json # 模块配置
├── testcases/ # 输出测试用例目录
├── utils/ # 工具函数
│ ├── logger.py # 日志工具
│ └── constants.py # 常量定义
└── README.md # 本文件
```
## 配置文件
### system_config.json
系统配置文件,包含登录信息:
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
```
### module_config.json
模块配置文件,指定要生成测试用例的模块:
```json
[
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加", "编辑", "删除"]
}
]
```
## 使用方法
### 基本用法
```bash
cd AuxiliaryTool/TestCaseGenerator
python main.py
```
### 指定配置文件
```bash
python main.py --system-config config/system_config.json --module-config config/module_config.json
```
### 选择登录类型
```bash
# 使用前台地址登录
python main.py --login-type front
# 使用后台地址登录(默认)
python main.py --login-type back
```
## 输出示例
生成的测试用例文件格式:
```json
{
"name": "区域管理_增值服务_添加",
"para": [
{
"page": "area/valueAddedService",
"step": "点击【添加】按钮",
"locator_type": "ID",
"locator_value": "addButton",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "area/valueAddedService",
"step": "输入服务名称",
"locator_type": "ID",
"locator_value": "serviceName",
"element_type": "input",
"element_value": "测试服务",
"expected_result": ""
},
{
"page": "area/valueAddedService",
"step": "点击【确定】按钮",
"locator_type": "XPATH",
"locator_value": "//span[contains(text(),'确定')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "area/valueAddedService",
"step": "获取提示文本",
"locator_type": "CSS_SELECTOR",
"locator_value": ".el-message__content",
"element_type": "getTips",
"element_value": "添加成功",
"expected_result": "添加成功"
}
],
"platform": "web",
"base_url": "https://192.168.5.44/"
}
```
## 支持的元素类型
| element_type | 说明 |
|-------------|------|
| click | 点击按钮、链接等可点击元素 |
| input | 文本输入框 |
| select | 下拉选择框 |
| checkbox | 复选框/单选框 |
| switch | 开关控件 |
| getTips | 获取提示信息 |
## 元素定位策略
按优先级自动选择最优定位方式:
1. **ID属性** - 优先使用唯一ID
2. **Name属性** - 其次使用name属性
3. **Class属性** - 使用class名称
4. **XPATH表达式** - 作为备选方案
5. **CSS选择器** - 作为备选方案
## 注意事项
1. **MCP依赖**:本工具需要chrome-devtools MCP服务运行
2. **验证码**:目前使用固定验证码"csba"
3. **测试数据**:编辑/删除操作会自动创建和清理测试数据
4. **日志文件**:运行日志保存在`logs/`目录下
## 开发规范
- 所有代码包含中文注释
- 符合PEP 8代码规范
- 使用类型注解
- 完善的错误处理和日志记录
## 更新日志
### V1.0 (2026-03-06)
- 初始版本
- 实现核心功能模块
- 支持添加/编辑/删除操作
- 智能元素定位
- JSON测试用例生成
## 相关文档
- 需求文档: `Docs/PRD/自动化测试用例生成/_PRD_自动化测试用例生成需求文档.md`
- 计划执行: `Docs/PRD/自动化测试用例生成/_PRD_自动化测试用例生成需求文档_计划执行.md`
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
# -*- coding: utf-8 -*-
"""
浏览器操作模块
封装chrome-devtools MCP工具,提供统一的浏览器操作接口
"""
import time
from typing import Dict, List, Optional, Any
from utils.logger import get_logger
from utils.constants import WAIT_TIMEOUT, WAIT_SHORT, MCP_TIMEOUT
class BrowserOperator:
"""浏览器操作器 - 封装chrome-devtools MCP工具"""
def __init__(self):
"""初始化浏览器操作器"""
self.logger = get_logger("BrowserOperator")
self.current_url: Optional[str] = None
self.page_id: Optional[int] = None
self.is_connected = False
def open_page(self, url: str, timeout: int = MCP_TIMEOUT) -> bool:
"""
打开新页面并导航到指定URL
Args:
url: 目标URL
timeout: 超时时间(毫秒)
Returns:
是否成功打开页面
Note:
此方法使用mcp__chrome-devtools__new_page工具
"""
self.logger.info(f"正在打开页面: {url}")
try:
# 注意:实际调用时需要使用MCP工具
# 这里是接口定义,实际实现在主程序中通过MCP调用
self.current_url = url
self.is_connected = True
self.logger.info(f"页面打开成功: {url}")
return True
except Exception as e:
self.logger.error(f"打开页面失败: {e}")
return False
def navigate(self, url: str, ignore_cache: bool = False) -> bool:
"""
在当前页面导航到指定URL
Args:
url: 目标URL
ignore_cache: 是否忽略缓存
Returns:
是否成功导航
"""
self.logger.info(f"正在导航到: {url}")
try:
self.current_url = url
self.logger.info(f"导航成功: {url}")
return True
except Exception as e:
self.logger.error(f"导航失败: {e}")
return False
def take_snapshot(self, verbose: bool = False) -> Dict:
"""
获取页面快照(可访问性树)
Args:
verbose: 是否包含详细信息
Returns:
页面快照字典,包含元素uid和相关信息
Note:
返回格式示例:
{
"elements": [
{
"uid": "123",
"role": "button",
"name": "确定",
"attributes": {...}
},
...
]
}
"""
self.logger.debug("正在获取页面快照")
try:
# 返回快照数据
snapshot = {}
self.logger.debug("页面快照获取成功")
return snapshot
except Exception as e:
self.logger.error(f"获取页面快照失败: {e}")
return {}
def find_element_by_text(self, snapshot: Dict, text: str, exact_match: bool = False) -> Optional[str]:
"""
在页面快照中查找包含指定文本的元素
Args:
snapshot: 页面快照
text: 要查找的文本
exact_match: 是否精确匹配
Returns:
元素的uid,如果未找到则返回None
"""
self.logger.debug(f"正在查找元素: {text}")
try:
elements = snapshot.get("elements", [])
for element in elements:
element_text = element.get("name", "")
if exact_match:
if element_text == text:
uid = element.get("uid")
self.logger.debug(f"找到元素: {text}, uid: {uid}")
return uid
else:
if text in element_text:
uid = element.get("uid")
self.logger.debug(f"找到元素: {text}, uid: {uid}")
return uid
self.logger.warning(f"未找到元素: {text}")
return None
except Exception as e:
self.logger.error(f"查找元素失败: {e}")
return None
def click_element(self, uid: str, wait_after: int = WAIT_SHORT) -> bool:
"""
点击指定元素
Args:
uid: 元素的唯一标识符
wait_after: 点击后等待时间(秒)
Returns:
是否成功点击
"""
self.logger.debug(f"正在点击元素: {uid}")
try:
# 等待页面响应
if wait_after > 0:
time.sleep(wait_after)
self.logger.debug(f"元素点击成功: {uid}")
return True
except Exception as e:
self.logger.error(f"点击元素失败: {e}")
return False
def fill_input(self, uid: str, value: str, clear_first: bool = True) -> bool:
"""
填写输入框
Args:
uid: 元素的唯一标识符
value: 要填写的值
clear_first: 是否先清空输入框
Returns:
是否成功填写
"""
self.logger.debug(f"正在填写输入框: {uid}, 值: {value}")
try:
self.logger.debug(f"输入框填写成功: {uid}")
return True
except Exception as e:
self.logger.error(f"填写输入框失败: {e}")
return False
def select_option(self, uid: str, value: str) -> bool:
"""
在下拉选择框中选择选项
Args:
uid: 元素的唯一标识符
value: 要选择的选项值
Returns:
是否成功选择
"""
self.logger.debug(f"正在选择下拉选项: {uid}, 值: {value}")
try:
self.logger.debug(f"下拉选项选择成功: {uid}")
return True
except Exception as e:
self.logger.error(f"选择下拉选项失败: {e}")
return False
def get_url(self) -> Optional[str]:
"""
获取当前页面URL
Returns:
当前页面URL
"""
return self.current_url
def wait_for_element(self, text: str, timeout: int = WAIT_TIMEOUT) -> bool:
"""
等待包含指定文本的元素出现
Args:
text: 要等待的元素文本
timeout: 超时时间(秒)
Returns:
元素是否出现
"""
self.logger.debug(f"正在等待元素出现: {text}, 超时: {timeout}秒")
start_time = time.time()
while time.time() - start_time < timeout:
snapshot = self.take_snapshot()
uid = self.find_element_by_text(snapshot, text)
if uid:
self.logger.debug(f"元素已出现: {text}")
return True
time.sleep(1)
self.logger.warning(f"等待元素超时: {text}")
return False
def wait_for_url_contains(self, url_fragment: str, timeout: int = WAIT_TIMEOUT) -> bool:
"""
等待URL包含指定片段
Args:
url_fragment: URL片段
timeout: 超时时间(秒)
Returns:
URL是否包含指定片段
"""
self.logger.debug(f"正在等待URL包含: {url_fragment}")
start_time = time.time()
while time.time() - start_time < timeout:
current_url = self.get_url()
if current_url and url_fragment in current_url:
self.logger.debug(f"URL已包含指定片段: {url_fragment}")
return True
time.sleep(1)
self.logger.warning(f"等待URL超时: {url_fragment}")
return False
def execute_script(self, script: str) -> Any:
"""
执行JavaScript脚本
Args:
script: JavaScript脚本
Returns:
脚本执行结果
"""
self.logger.debug(f"正在执行脚本: {script[:50]}...")
try:
result = None
self.logger.debug("脚本执行成功")
return result
except Exception as e:
self.logger.error(f"脚本执行失败: {e}")
return None
def get_element_attributes(self, uid: str) -> Dict:
"""
获取元素属性
Args:
uid: 元素的唯一标识符
Returns:
元素属性字典
"""
self.logger.debug(f"正在获取元素属性: {uid}")
try:
attributes = {}
self.logger.debug(f"元素属性获取成功: {uid}")
return attributes
except Exception as e:
self.logger.error(f"获取元素属性失败: {e}")
return {}
def close(self) -> None:
"""关闭浏览器"""
self.logger.info("正在关闭浏览器")
try:
self.is_connected = False
self.current_url = None
self.logger.info("浏览器已关闭")
except Exception as e:
self.logger.error(f"关闭浏览器失败: {e}")
[
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加", "编辑", "删除"]
}
]
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
# -*- coding: utf-8 -*-
"""
配置管理模块
负责读取、验证和管理配置文件
"""
import json
from pathlib import Path
from typing import List, Dict, Optional
from utils.logger import get_logger
class ConfigManager:
"""配置管理器"""
def __init__(self, config_dir: Optional[Path] = None):
"""
初始化配置管理器
Args:
config_dir: 配置文件目录,默认为当前目录下的config文件夹
"""
self.logger = get_logger("ConfigManager")
if config_dir is None:
# 默认配置目录为当前脚本目录下的config文件夹
self.config_dir = Path(__file__).parent / "config"
else:
self.config_dir = Path(config_dir)
self.system_config: Optional[List[Dict]] = None
self.module_config: Optional[List[Dict]] = None
def load_system_config(self, config_file: str = "system_config.json") -> List[Dict]:
"""
加载系统配置文件
Args:
config_file: 配置文件名或相对/绝对路径
Returns:
系统配置列表
Raises:
FileNotFoundError: 配置文件不存在
json.JSONDecodeError: JSON格式错误
ValueError: 配置格式验证失败
"""
# 如果传入的是绝对路径,直接使用
config_path = Path(config_file)
if not config_path.is_absolute():
# 相对路径:相对于config目录
config_path = self.config_dir / Path(config_file).name
if not config_path.exists():
raise FileNotFoundError(f"系统配置文件不存在: {config_path}")
self.logger.info(f"正在加载系统配置文件: {config_path}")
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
except json.JSONDecodeError as e:
self.logger.error(f"系统配置文件JSON格式错误: {e}")
raise
# 验证配置格式
if not self._validate_system_config(config):
raise ValueError("系统配置格式验证失败")
self.system_config = config
self.logger.info(f"系统配置加载成功,共 {len(config)} 个系统配置")
return config
def load_module_config(self, config_file: str = "module_config.json") -> List[Dict]:
"""
加载模块配置文件
Args:
config_file: 配置文件名或相对/绝对路径
Returns:
模块配置列表
Raises:
FileNotFoundError: 配置文件不存在
json.JSONDecodeError: JSON格式错误
ValueError: 配置格式验证失败
"""
# 如果传入的是绝对路径,直接使用
config_path = Path(config_file)
if not config_path.is_absolute():
# 相对路径:相对于config目录
config_path = self.config_dir / Path(config_file).name
if not config_path.exists():
raise FileNotFoundError(f"模块配置文件不存在: {config_path}")
self.logger.info(f"正在加载模块配置文件: {config_path}")
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
except json.JSONDecodeError as e:
self.logger.error(f"模块配置文件JSON格式错误: {e}")
raise
# 验证配置格式
if not self._validate_module_config(config):
raise ValueError("模块配置格式验证失败")
self.module_config = config
self.logger.info(f"模块配置加载成功,共 {len(config)} 个模块配置")
return config
def _validate_system_config(self, config: List[Dict]) -> bool:
"""
验证系统配置格式
Args:
config: 系统配置字典
Returns:
验证是否通过
"""
if not isinstance(config, list):
self.logger.error("系统配置必须是列表格式")
return False
required_fields = [
"system_type",
"system_front_url",
"system_back_url",
"username",
"password",
"code"
]
for idx, system in enumerate(config):
if not isinstance(system, dict):
self.logger.error(f"系统配置第{idx+1}项必须是字典格式")
return False
# 检查必填字段
for field in required_fields:
if field not in system:
self.logger.error(f"系统配置第{idx+1}项缺少必填字段: {field}")
return False
# 验证URL格式
if not system["system_front_url"].startswith(("http://", "https://")):
self.logger.error(f"系统配置第{idx+1}项的system_front_url格式错误")
return False
if not system["system_back_url"].startswith(("http://", "https://")):
self.logger.error(f"系统配置第{idx+1}项的system_back_url格式错误")
return False
return True
def _validate_module_config(self, config: List[Dict]) -> bool:
"""
验证模块配置格式
Args:
config: 模块配置字典
Returns:
验证是否通过
"""
if not isinstance(config, list):
self.logger.error("模块配置必须是列表格式")
return False
required_fields = ["module_name", "module_name_son", "module_function"]
for idx, module in enumerate(config):
if not isinstance(module, dict):
self.logger.error(f"模块配置第{idx+1}项必须是字典格式")
return False
# 检查必填字段
for field in required_fields:
if field not in module:
self.logger.error(f"模块配置第{idx+1}项缺少必填字段: {field}")
return False
# 验证module_function是列表
if not isinstance(module["module_function"], list):
self.logger.error(f"模块配置第{idx+1}项的module_function必须是列表格式")
return False
# 验证功能类型
valid_functions = ["添加", "编辑", "删除"]
for func in module["module_function"]:
if func not in valid_functions:
self.logger.warning(
f"模块配置第{idx+1}项包含未知功能类型: {func},"
f"支持的类型: {valid_functions}"
)
return True
def get_system_config(self, index: int = 0) -> Dict:
"""
获取指定索引的系统配置
Args:
index: 配置索引,默认为0
Returns:
系统配置字典
Raises:
IndexError: 索引超出范围
ValueError: 配置未加载
"""
if self.system_config is None:
raise ValueError("系统配置未加载,请先调用load_system_config()")
if index < 0 or index >= len(self.system_config):
raise IndexError(f"系统配置索引超出范围: {index}")
return self.system_config[index]
def get_module_configs(self) -> List[Dict]:
"""
获取所有模块配置
Returns:
模块配置列表
Raises:
ValueError: 配置未加载
"""
if self.module_config is None:
raise ValueError("模块配置未加载,请先调用load_module_config()")
return self.module_config
def get_output_dir(self) -> Path:
"""
获取测试用例输出目录
Returns:
输出目录路径
"""
# 输出目录为配置文件同目录下的testcases文件夹
return self.config_dir / "testcases"
# -*- coding: utf-8 -*-
"""
测试用例生成演示脚本
通过chrome-devtools MCP工具演示完整的测试用例生成流程
"""
import json
import time
from pathlib import Path
from typing import Dict, List, Optional
class TestCaseDemo:
"""测试用例生成演示类"""
def __init__(self):
"""初始化演示类"""
self.steps = []
self.current_page = ""
self.current_route = ""
def log(self, message: str):
"""打印日志"""
print(f"[{time.strftime('%H:%M:%S')}] {message}")
def navigate_to_baidu(self):
"""导航到百度首页"""
self.log("步骤1: 导航到百度首页")
result = mcp__chrome_devtools__navigate_page(type="url", url="https://www.baidu.com")
time.sleep(2)
self.current_page = "https://www.baidu.com"
self.log(f"✓ 成功导航到: {self.current_page}")
return True
def take_snapshot_and_analyze(self):
"""获取页面快照并分析"""
self.log("步骤2: 获取页面快照")
snapshot = mcp__chrome_devtools__take_snapshot()
self.log(f"✓ 快照获取成功,元素数量: {len(str(snapshot).split('uid='))}")
return snapshot
def find_search_box(self, snapshot: Dict):
"""查找搜索框元素"""
self.log("步骤3: 查找搜索框元素")
# 在快照中查找搜索框
snapshot_text = str(snapshot)
# 查找搜索框uid
if "uid=1_23 textbox" in snapshot_text:
search_box_uid = "1_23"
self.log(f"✓ 找到搜索框: uid={search_box_uid}")
# 记录步骤
self.add_step(
page="baidu/home",
step="定位搜索框",
locator_type="UID",
locator_value="1_23",
element_type="input",
element_value=""
)
return search_box_uid
self.log("✗ 未找到搜索框")
return None
def fill_search_box(self, uid: str, text: str):
"""填写搜索框"""
self.log(f"步骤4: 填写搜索框,内容: {text}")
# 使用JavaScript填写
script = f"(text) => {{ const input = document.getElementById('kw'); if (input) {{ input.value = text; input.dispatchEvent(new Event('input', {{ bubbles: true }})); return {{success: true, value: text}}; }} return {{success: false}}; }}"
result = mcp__chrome_devtools__evaluate_script(function=script, args=[])
self.log(f"✓ 填写完成: {result}")
# 记录步骤
self.add_step(
page="baidu/home",
step=f"输入搜索内容: {text}",
locator_type="ID",
locator_value="kw",
element_type="input",
element_value=text
)
return True
def find_search_button(self, snapshot: Dict):
"""查找搜索按钮"""
self.log("步骤5: 查找搜索按钮")
snapshot_text = str(snapshot)
if "uid=1_24 button" in snapshot_text and "百度一下" in snapshot_text:
button_uid = "1_24"
self.log(f"✓ 找到搜索按钮: uid={button_uid}")
# 记录步骤
self.add_step(
page="baidu/home",
step="定位搜索按钮",
locator_type="UID",
locator_value="1_24",
element_type="click",
element_value=""
)
return button_uid
self.log("✗ 未找到搜索按钮")
return None
def click_search_button(self, uid: str):
"""点击搜索按钮"""
self.log("步骤6: 点击搜索按钮")
# 使用JavaScript点击
script = "() => { const btn = document.getElementById('su'); if (btn) { btn.click(); return {success: true}; } return {success: false}; }"
result = mcp__chrome_devtools__evaluate_script(function=script, args=[])
self.log(f"✓ 点击完成: {result}")
# 记录步骤
self.add_step(
page="baidu/home",
step="点击【百度一下】按钮",
locator_type="ID",
locator_value="su",
element_type="click",
element_value=""
)
time.sleep(2)
return True
def verify_search_results(self):
"""验证搜索结果页面"""
self.log("步骤7: 验证搜索结果页面")
# 获取当前URL
script = "() => { return window.location.href; }"
result = mcp__chrome_devtools__evaluate_script(function=script, args=[])
self.log(f"✓ 当前URL: {result}")
if "s?" in result or "wd=" in result:
self.log("✓ 成功跳转到搜索结果页面")
# 记录验证步骤
self.add_step(
page="baidu/search",
step="验证搜索结果页面",
locator_type="URL",
locator_value="contains s?",
element_type="getTips",
element_value="搜索成功",
expected_result="跳转到搜索结果页面"
)
return True
return False
def add_step(self, page: str, step: str, locator_type: str,
locator_value: str, element_type: str,
element_value: str, expected_result: str = ""):
"""添加测试步骤"""
self.steps.append({
"page": page,
"step": step,
"locator_type": locator_type,
"locator_value": locator_value,
"element_type": element_type,
"element_value": element_value,
"expected_result": expected_result
})
def generate_testcase(self) -> Dict:
"""生成测试用例"""
self.log("生成测试用例JSON")
testcase = {
"name": "百度_首页_搜索",
"para": self.steps,
"platform": "web",
"base_url": "https://www.baidu.com"
}
return testcase
def save_testcase(self, testcase: Dict, output_path: str):
"""保存测试用例到文件"""
self.log(f"保存测试用例到: {output_path}")
output_file = Path(output_path)
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(testcase, f, ensure_ascii=False, indent=2)
self.log(f"✓ 测试用例已保存")
def run(self):
"""运行完整演示"""
self.log("=" * 60)
self.log("测试用例生成演示开始")
self.log("=" * 60)
try:
# 1. 导航到百度
if not self.navigate_to_baidu():
return False
# 2. 获取快照
snapshot = self.take_snapshot_and_analyze()
# 3. 查找搜索框
search_box_uid = self.find_search_box(snapshot)
if not search_box_uid:
return False
# 4. 填写搜索框
self.fill_search_box(search_box_uid, "自动化测试")
# 5. 查找搜索按钮
button_uid = self.find_search_button(snapshot)
if not button_uid:
return False
# 6. 点击搜索按钮
self.click_search_button(button_uid)
# 7. 验证搜索结果
self.verify_search_results()
# 8. 生成测试用例
testcase = self.generate_testcase()
# 9. 保存测试用例
output_path = "E:/GithubData/ubains-module-test/AuxiliaryTool/TestCaseGenerator/testcases/百度_首页_搜索.json"
self.save_testcase(testcase, output_path)
# 10. 打印测试用例
self.log("\n生成的测试用例:")
self.log("-" * 60)
print(json.dumps(testcase, ensure_ascii=False, indent=2))
self.log("\n" + "=" * 60)
self.log("演示完成!")
self.log("=" * 60)
return True
except Exception as e:
self.log(f"✗ 演示过程中发生错误: {e}")
return False
def main():
"""主函数"""
demo = TestCaseDemo()
success = demo.run()
return 0 if success else 1
# 此脚本需要在Claude Code环境中运行,以访问MCP工具
# 使用方法:在Claude Code中执行此脚本的内容
# -*- coding: utf-8 -*-
"""
元素定位模块
负责智能检测元素定位方式,按优先级选择最优定位策略
"""
from typing import Dict, List, Optional, Tuple
from browser_operator import BrowserOperator
from utils.logger import get_logger
from utils.constants import LOCATOR_PRIORITIES
class ElementLocator:
"""元素定位器 - 智能检测元素定位方式"""
def __init__(self, browser: BrowserOperator):
"""
初始化元素定位器
Args:
browser: 浏览器操作器实例
"""
self.browser = browser
self.logger = get_logger("ElementLocator")
def locate_element(self, element_info: Dict) -> Dict:
"""
定位元素并生成最优定位策略
Args:
element_info: 元素信息字典,包含uid、text等
Returns:
定位结果字典,包含locator_type、locator_value等
"""
uid = element_info.get("uid")
if not uid:
self.logger.error("元素信息中缺少uid")
return {}
self.logger.debug(f"正在分析元素定位方式: {uid}")
# 获取元素属性
attributes = self.browser.get_element_attributes(uid)
# 按优先级分析定位策略
for strategy in LOCATOR_PRIORITIES:
locator_value = self._generate_locator_value(attributes, strategy)
if locator_value:
result = {
"locator_type": strategy,
"locator_value": locator_value,
"backup_locators": self._generate_backup_locators(attributes, strategy),
"attributes": attributes
}
self.logger.debug(f"选择定位策略: {strategy}, 值: {locator_value}")
return result
# 如果所有策略都失败,返回空
self.logger.warning(f"未能为元素生成定位策略: {uid}")
return {}
def _generate_locator_value(self, attributes: Dict, strategy: str) -> Optional[str]:
"""
根据策略生成定位值
Args:
attributes: 元素属性字典
strategy: 定位策略
Returns:
定位值,生成失败返回None
"""
if strategy == "ID":
return self._generate_id_locator(attributes)
elif strategy == "NAME":
return self._generate_name_locator(attributes)
elif strategy == "CLASS":
return self._generate_class_locator(attributes)
elif strategy == "XPATH":
return self._generate_xpath_locator(attributes)
elif strategy == "CSS_SELECTOR":
return self._generate_css_selector(attributes)
return None
def _generate_id_locator(self, attributes: Dict) -> Optional[str]:
"""
生成ID定位器
Args:
attributes: 元素属性字典
Returns:
ID定位值,如果没有ID返回None
"""
element_id = attributes.get("id")
if not element_id:
return None
# 验证ID是否唯一(非空且不是动态生成的)
if self._is_valid_id(element_id):
self.logger.debug(f"使用ID定位: {element_id}")
return element_id
return None
def _generate_name_locator(self, attributes: Dict) -> Optional[str]:
"""
生成Name定位器
Args:
attributes: 元素属性字典
Returns:
Name定位值,如果没有name返回None
"""
name = attributes.get("name")
if not name:
return None
self.logger.debug(f"使用Name定位: {name}")
return name
def _generate_class_locator(self, attributes: Dict) -> Optional[str]:
"""
生成Class定位器
Args:
attributes: 元素属性字典
Returns:
Class定位值,如果没有class返回None
"""
class_list = attributes.get("class")
if not class_list:
return None
# 如果是字符串,按空格分割
if isinstance(class_list, str):
classes = class_list.split()
else:
classes = class_list
# 优先使用第一个class
if classes:
first_class = classes[0].strip()
# 过滤掉动态生成的class(如包含数字的)
if not self._is_dynamic_class(first_class):
self.logger.debug(f"使用Class定位: {first_class}")
return first_class
return None
def _generate_xpath_locator(self, attributes: Dict) -> Optional[str]:
"""
生成XPATH定位器
Args:
attributes: 元素属性字典
Returns:
XPATH定位值
"""
# 优先使用包含文本的XPATH
text = attributes.get("text", "")
tag = attributes.get("tag", "*")
if text:
# 使用包含文本的XPATH(使用 . 而不是 text() 以匹配Element UI按钮span子元素内的文本)
xpath = f"//{tag}[contains(.,'{text}')]"
self.logger.debug(f"使用XPATH定位(文本): {xpath}")
return xpath
# 如果有ID,使用ID的XPATH
element_id = attributes.get("id")
if element_id:
xpath = f"//{tag}[@id='{element_id}']"
self.logger.debug(f"使用XPATH定位(ID): {xpath}")
return xpath
# 如果有name,使用name的XPATH
name = attributes.get("name")
if name:
xpath = f"//{tag}[@name='{name}']"
self.logger.debug(f"使用XPATH定位(name): {xpath}")
return xpath
# 如果有placeholder,使用placeholder的XPATH
placeholder = attributes.get("placeholder")
if placeholder:
xpath = f"//{tag}[@placeholder='{placeholder}']"
self.logger.debug(f"使用XPATH定位(placeholder): {xpath}")
return xpath
# 默认使用标签名
xpath = f"//{tag}"
self.logger.debug(f"使用XPATH定位(标签): {xpath}")
return xpath
def _generate_css_selector(self, attributes: Dict) -> Optional[str]:
"""
生成CSS选择器定位器
Args:
attributes: 元素属性字典
Returns:
CSS选择器定位值
"""
selectors = []
tag = attributes.get("tag", "*")
# 添加标签名
if tag != "*":
selectors.append(tag)
# 添加ID
element_id = attributes.get("id")
if element_id:
selectors.append(f"#{element_id}")
# 添加class
class_list = attributes.get("class")
if class_list:
if isinstance(class_list, str):
classes = class_list.split()
else:
classes = class_list
if classes:
first_class = classes[0].strip()
if not self._is_dynamic_class(first_class):
selectors.append(f".{first_class}")
# 添加属性选择器
name = attributes.get("name")
if name:
selectors.append(f"[name='{name}']")
# 组合选择器
if selectors:
css_selector = "".join(selectors)
self.logger.debug(f"使用CSS选择器: {css_selector}")
return css_selector
return None
def _generate_backup_locators(self, attributes: Dict, primary_strategy: str) -> List[Dict]:
"""
生成备用定位方式
Args:
attributes: 元素属性字典
primary_strategy: 主定位策略
Returns:
备用定位器列表
"""
backup_locators = []
for strategy in LOCATOR_PRIORITIES:
# 跳过主策略
if strategy == primary_strategy:
continue
locator_value = self._generate_locator_value(attributes, strategy)
if locator_value:
backup_locators.append({
"locator_type": strategy,
"locator_value": locator_value
})
return backup_locators
def _is_valid_id(self, element_id: str) -> bool:
"""
验证ID是否有效(非动态生成)
Args:
element_id: 元素ID
Returns:
ID是否有效
"""
# 空值无效
if not element_id:
return False
# 动态ID通常包含数字或特殊字符模式
# 这里简单判断:不包含连续3位以上数字
import re
if re.search(r'\d{3,}', element_id):
return False
return True
def _is_dynamic_class(self, class_name: str) -> bool:
"""
判断class是否是动态生成的
Args:
class_name: 类名
Returns:
是否是动态class
"""
# 动态class通常包含hash或随机字符串
# 这里简单判断:包含数字或特殊下划线
if any(char in class_name for char in ['_', '-']):
# 如果同时包含数字,很可能是动态生成的
if any(char.isdigit() for char in class_name):
return True
return False
def analyze_element_from_snapshot(self, snapshot: Dict, text: str) -> Dict:
"""
从页面快照中分析元素
Args:
snapshot: 页面快照
text: 元素文本
Returns:
元素分析结果
"""
self.logger.debug(f"正在从快照分析元素: {text}")
# 查找元素
uid = self.browser.find_element_by_text(snapshot, text)
if not uid:
self.logger.warning(f"未找到元素: {text}")
return {}
# 获取元素属性
attributes = self.browser.get_element_attributes(uid)
# 分析定位策略
element_info = {
"uid": uid,
"text": text,
"attributes": attributes
}
return self.locate_element(element_info)
def determine_element_type(self, attributes: Dict) -> Optional[str]:
"""
确定元素类型
Args:
attributes: 元素属性字典
Returns:
元素类型(click/input/select/checkbox/switch/getTips/getText)
- getTips: 弹窗提示文本
- getText: 列表搜索后文本获取
"""
tag = attributes.get("tag", "")
role = attributes.get("role", "")
input_type = attributes.get("type", "")
# 输入框
if tag == "input" or tag == "textarea":
if input_type == "checkbox":
return "checkbox"
elif input_type == "radio":
return "checkbox"
else:
return "input"
# 下拉选择框
if tag == "select":
return "select"
# 按钮或链接
if tag == "button" or tag == "a":
return "click"
# 根据ARIA角色判断
if role == "button":
return "click"
elif role == "textbox":
return "input"
elif role == "combobox":
return "select"
elif role == "switch":
return "switch"
elif role == "checkbox":
return "checkbox"
# 默认为可点击元素
return "click"
# -*- coding: utf-8 -*-
"""
登录处理模块
负责处理系统登录流程
"""
from typing import Dict, Optional
from browser_operator import BrowserOperator
from utils.logger import get_logger
from utils.constants import WAIT_TIMEOUT, WAIT_SHORT
class LoginHandler:
"""登录处理器"""
def __init__(self, browser: BrowserOperator):
"""
初始化登录处理器
Args:
browser: 浏览器操作器实例
"""
self.browser = browser
self.logger = get_logger("LoginHandler")
def login(self, url: str, username: str, password: str, code: str = "csba") -> bool:
"""
执行登录操作
Args:
url: 登录页面URL
username: 用户名
password: 密码
code: 验证码,默认为csba
Returns:
是否登录成功
"""
self.logger.info(f"开始登录流程,用户: {username}")
# 1. 打开登录页面
self.logger.info("正在打开登录页面...")
if not self.browser.open_page(url):
self.logger.error("打开登录页面失败")
return False
# 等待页面加载
import time
time.sleep(WAIT_SHORT)
# 2. 获取页面快照
snapshot = self.browser.take_snapshot()
# 3. 查找并填写用户名
username_uid = self._find_username_input(snapshot)
if not username_uid:
self.logger.error("未找到用户名输入框")
return False
self.logger.info(f"正在填写用户名: {username}")
if not self.browser.fill_input(username_uid, username):
self.logger.error("填写用户名失败")
return False
# 4. 查找并填写密码
password_uid = self._find_password_input(snapshot)
if not password_uid:
self.logger.error("未找到密码输入框")
return False
self.logger.info("正在填写密码")
if not self.browser.fill_input(password_uid, password):
self.logger.error("填写密码失败")
return False
# 5. 查找并填写验证码
code_uid = self._find_code_input(snapshot)
if code_uid:
self.logger.info(f"正在填写验证码: {code}")
if not self.browser.fill_input(code_uid, code):
self.logger.warning("填写验证码失败,继续尝试登录")
else:
self.logger.warning("未找到验证码输入框,可能不需要验证码")
# 6. 查找并点击登录按钮
login_button_uid = self._find_login_button(snapshot)
if not login_button_uid:
self.logger.error("未找到登录按钮")
return False
self.logger.info("正在点击登录按钮")
if not self.browser.click_element(login_button_uid):
self.logger.error("点击登录按钮失败")
return False
# 7. 等待登录完成
self.logger.info("等待登录完成...")
time.sleep(WAIT_SHORT)
# 8. 验证登录成功
if self.verify_login_success():
self.logger.info("登录成功")
return True
else:
self.logger.error("登录验证失败")
return False
def _find_username_input(self, snapshot: Dict) -> Optional[str]:
"""
查找用户名输入框
Args:
snapshot: 页面快照
Returns:
用户名输入框的uid,未找到返回None
"""
# 常见的用户名输入框标识
username_keywords = ["用户名", "账号", "username", "account", "用户"]
for keyword in username_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
return uid
return None
def _find_password_input(self, snapshot: Dict) -> Optional[str]:
"""
查找密码输入框
Args:
snapshot: 页面快照
Returns:
密码输入框的uid,未找到返回None
"""
# 常见的密码输入框标识
password_keywords = ["密码", "password", "pwd"]
for keyword in password_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
return uid
return None
def _find_code_input(self, snapshot: Dict) -> Optional[str]:
"""
查找验证码输入框
Args:
snapshot: 页面快照
Returns:
验证码输入框的uid,未找到返回None
"""
# 常见的验证码输入框标识
code_keywords = ["验证码", "code", "captcha"]
for keyword in code_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
return uid
return None
def _find_login_button(self, snapshot: Dict) -> Optional[str]:
"""
查找登录按钮
Args:
snapshot: 页面快照
Returns:
登录按钮的uid,未找到返回None
"""
# 常见的登录按钮标识
login_keywords = ["登录", "login", "登 录", "提交"]
for keyword in login_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
return uid
return None
def verify_login_success(self) -> bool:
"""
验证登录是否成功
Returns:
是否登录成功
"""
self.logger.debug("正在验证登录状态")
# 方法1:检查URL是否已跳转
current_url = self.browser.get_url()
# 如果URL不包含login或Login,说明已登录
if current_url and "login" not in current_url.lower() and "Login" not in current_url:
self.logger.debug("URL已跳转,登录验证通过")
return True
# 方法2:检查页面是否包含登录后的标识元素
# 如:用户信息、退出按钮等
snapshot = self.browser.take_snapshot()
success_indicators = ["退出", "logout", "用户信息", "首页", "home"]
for indicator in success_indicators:
uid = self.browser.find_element_by_text(snapshot, indicator)
if uid:
self.logger.debug(f"找到登录后标识元素: {indicator}")
return True
self.logger.warning("未能通过常规方式验证登录状态")
return False
def logout(self) -> bool:
"""
退出登录
Returns:
是否退出成功
"""
self.logger.info("开始退出登录")
try:
snapshot = self.browser.take_snapshot()
# 查找退出按钮
logout_keywords = ["退出", "logout", "注销"]
for keyword in logout_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
self.logger.info("正在点击退出按钮")
if self.browser.click_element(uid):
self.logger.info("退出登录成功")
return True
self.logger.warning("未找到退出按钮")
return False
except Exception as e:
self.logger.error(f"退出登录失败: {e}")
return False
# -*- coding: utf-8 -*-
"""
自动化测试用例生成工具 - 主程序
通过Claude Code + MCP自动生成测试用例
"""
import sys
import time
from pathlib import Path
from typing import Dict, List, Optional
# 添加项目根目录到路径
sys.path.insert(0, str(Path(__file__).parent))
from config_manager import ConfigManager
from browser_operator import BrowserOperator
from login_handler import LoginHandler
from navigation_handler import NavigationHandler
from element_locator import ElementLocator
from operation_executor import OperationExecutor
from testcase_generator import TestCaseGenerator
from utils.logger import get_logger, setup_file_logger
from utils.constants import LOGS_DIR
class TestCaseGeneratorMain:
"""测试用例生成器主控类"""
def __init__(self, system_config_path: str = "config/system_config.json",
module_config_path: str = "config/module_config.json"):
"""
初始化主控类
Args:
system_config_path: 系统配置文件路径
module_config_path: 模块配置文件路径
"""
self.logger = get_logger("TestCaseGeneratorMain")
self.config_manager = ConfigManager()
self.browser: Optional[BrowserOperator] = None
self.login_handler: Optional[LoginHandler] = None
self.navigation_handler: Optional[NavigationHandler] = None
self.element_locator: Optional[ElementLocator] = None
self.operation_executor: Optional[OperationExecutor] = None
# 配置文件路径
self.system_config_path = system_config_path
self.module_config_path = module_config_path
# 设置文件日志
project_root = Path(__file__).parent
log_dir = project_root / LOGS_DIR
setup_file_logger(self.logger, log_dir)
def run(self) -> None:
"""执行主流程"""
self.logger.info("=" * 50)
self.logger.info("自动化测试用例生成工具启动")
self.logger.info("=" * 50)
try:
# 1. 加载配置
self._load_configs()
# 2. 选择登录URL
system_config = self.config_manager.get_system_config()
login_url = self._select_login_url(system_config)
# 3. 初始化浏览器
self._initialize_browser()
# 4. 登录系统
self._login_system(login_url, system_config)
# 5. 处理所有模块
self._process_all_modules()
# 6. 生成完成
self.logger.info("=" * 50)
self.logger.info("测试用例生成完成!")
self.logger.info("=" * 50)
except Exception as e:
self.logger.error(f"执行过程中发生错误: {e}")
raise
finally:
# 清理资源
self._cleanup()
def _load_configs(self) -> None:
"""加载配置文件"""
self.logger.info("正在加载配置文件...")
# 加载系统配置
self.config_manager.load_system_config(self.system_config_path)
system_config = self.config_manager.get_system_config()
self.logger.info(f"系统: {system_config.get('system_type')}")
self.logger.info(f"前台地址: {system_config.get('system_front_url')}")
self.logger.info(f"后台地址: {system_config.get('system_back_url')}")
# 加载模块配置
self.config_manager.load_module_config(self.module_config_path)
module_configs = self.config_manager.get_module_configs()
self.logger.info(f"待处理模块数量: {len(module_configs)}")
for idx, module in enumerate(module_configs, 1):
module_name = module.get("module_name")
module_name_son = module.get("module_name_son")
functions = module.get("module_function")
self.logger.info(f" 模块{idx}: {module_name} > {module_name_son}, 功能: {functions}")
def _select_login_url(self, system_config: Dict) -> str:
"""
选择登录URL
Args:
system_config: 系统配置字典
Returns:
登录URL
"""
self.logger.info("请选择登录地址:")
self.logger.info(" 1. 前台地址")
self.logger.info(" 2. 后台地址")
# 简化处理:默认使用后台地址
# 实际使用时可以通过命令行参数或交互式输入选择
login_url = system_config.get("system_back_url")
self.logger.info(f"使用后台地址登录: {login_url}")
return login_url
def _initialize_browser(self) -> None:
"""初始化浏览器"""
self.logger.info("正在初始化浏览器...")
self.browser = BrowserOperator()
# 初始化各个处理器
self.element_locator = ElementLocator(self.browser)
self.operation_executor = OperationExecutor(self.browser, self.element_locator)
self.logger.info("浏览器初始化完成")
def _login_system(self, login_url: str, system_config: Dict) -> None:
"""
登录系统
Args:
login_url: 登录URL
system_config: 系统配置字典
"""
self.logger.info("正在登录系统...")
self.login_handler = LoginHandler(self.browser)
username = system_config.get("username")
password = system_config.get("password")
code = system_config.get("code", "csba")
if not self.login_handler.login(login_url, username, password, code):
raise Exception("登录失败")
# 初始化导航处理器
self.navigation_handler = NavigationHandler(self.browser)
def _process_all_modules(self) -> None:
"""处理所有模块配置"""
module_configs = self.config_manager.get_module_configs()
output_dir = self.config_manager.get_output_dir()
for idx, module_config in enumerate(module_configs, 1):
self.logger.info(f"\n正在处理第 {idx}/{len(module_configs)} 个模块...")
self._process_single_module(module_config, output_dir)
def _process_single_module(self, module_config: Dict, output_dir: Path) -> None:
"""
处理单个模块
Args:
module_config: 模块配置字典
output_dir: 输出目录
"""
module_name = module_config.get("module_name")
module_name_son = module_config.get("module_name_son")
module_functions = module_config.get("module_function")
self.logger.info(f"处理模块: {module_name} > {module_name_son}")
self.logger.info(f"功能列表: {module_functions}")
# 1. 导航到指定模块
success, route = self.navigation_handler.navigate_to_module(
module_name, module_name_son
)
if not success:
self.logger.error(f"导航到模块失败: {module_name} > {module_name_son}")
return
if not route:
route = f"{module_name}/{module_name_son}"
self.logger.warning(f"未能获取路由,使用默认路由: {route}")
# 2. 初始化测试用例生成器
system_config = self.config_manager.get_system_config()
base_url = system_config.get("system_front_url", "")
testcase_generator = TestCaseGenerator(module_config, base_url)
# 3. 执行各功能并生成测试用例
steps_dict = {}
for function in module_functions:
self.logger.info(f"\n执行功能: {function}")
if function == "添加":
steps = self.operation_executor.execute_add(route)
elif function == "编辑":
steps = self.operation_executor.execute_edit(route)
elif function == "删除":
steps = self.operation_executor.execute_delete(route)
else:
self.logger.warning(f"未知功能类型: {function}")
continue
if steps:
steps_dict[function] = steps
self.logger.info(f"功能 {function} 执行完成,共 {len(steps)} 个步骤")
else:
self.logger.warning(f"功能 {function} 没有生成步骤")
# 4. 批量生成测试用例
if steps_dict:
filepaths = testcase_generator.batch_generate(
module_functions, steps_dict, output_dir
)
self.logger.info(f"模块 {module_name} > {module_name_son} 处理完成")
self.logger.info(f"生成文件: {len(filepaths)} 个")
for filepath in filepaths:
self.logger.info(f" - {filepath}")
else:
self.logger.warning(f"模块 {module_name} > {module_name_son} 没有生成任何测试用例")
def _cleanup(self) -> None:
"""清理资源"""
self.logger.info("正在清理资源...")
if self.browser:
self.browser.close()
self.logger.info("资源清理完成")
def main():
"""主函数入口"""
import argparse
parser = argparse.ArgumentParser(description="自动化测试用例生成工具")
parser.add_argument(
"--system-config",
default="config/system_config.json",
help="系统配置文件路径"
)
parser.add_argument(
"--module-config",
default="config/module_config.json",
help="模块配置文件路径"
)
parser.add_argument(
"--login-type",
choices=["front", "back"],
default="back",
help="登录地址类型(front: 前台, back: 后台)"
)
args = parser.parse_args()
# 创建并运行主控类
main_controller = TestCaseGeneratorMain(
system_config_path=args.system_config,
module_config_path=args.module_config
)
try:
main_controller.run()
return 0
except Exception as e:
print(f"错误: {e}")
return 1
if __name__ == "__main__":
sys.exit(main())
# -*- coding: utf-8 -*-
"""
MCP工具包装器模块
提供chrome-devtools MCP工具的调用接口
"""
import json
from typing import Dict, List, Optional, Any
from utils.logger import get_logger
class MCPBrowserWrapper:
"""
MCP浏览器包装器
注意:此类需要通过Claude Code的MCP工具实际调用。
在实际使用时,需要将方法调用传递给Claude Code的MCP工具。
"""
def __init__(self):
"""初始化MCP包装器"""
self.logger = get_logger("MCPBrowserWrapper")
self.page_id: Optional[int] = None
def new_page(self, url: str, background: bool = False,
timeout: int = 30000) -> Optional[Dict]:
"""
打开新页面
Args:
url: 目标URL
background: 是否在后台打开
timeout: 超时时间(毫秒)
Returns:
页面信息字典
"""
self.logger.info(f"[MCP调用] new_page: {url}")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__new_page(url=url, background=background, timeout=timeout)
return {
"pageId": self.page_id,
"url": url
}
def navigate(self, url: str, ignore_cache: bool = False,
timeout: int = 30000) -> bool:
"""
导航到指定URL
Args:
url: 目标URL
ignore_cache: 是否忽略缓存
timeout: 超时时间(毫秒)
Returns:
是否成功
"""
self.logger.info(f"[MCP调用] navigate: {url}")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__navigate_page(type="url", url=url, ignore_cache=ignore_cache)
return True
def take_snapshot(self, verbose: bool = False,
file_path: Optional[str] = None) -> Dict:
"""
获取页面快照
Args:
verbose: 是否包含详细信息
file_path: 快照保存路径
Returns:
页面快照字典
"""
self.logger.debug("[MCP调用] take_snapshot")
# 实际调用需要通过Claude Code的MCP工具
# result = mcp__chrome_devtools__take_snapshot(verbose=verbose, filePath=file_path)
# 返回模拟的快照结构
return {
"elements": [],
"url": "",
"title": ""
}
def click(self, uid: str, dbl_click: bool = False,
include_snapshot: bool = False) -> bool:
"""
点击元素
Args:
uid: 元素uid
dbl_click: 是否双击
include_snapshot: 是否包含快照
Returns:
是否成功
"""
self.logger.debug(f"[MCP调用] click: {uid}")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__click(uid=uid, dblClick=dbl_click, includeSnapshot=include_snapshot)
return True
def fill(self, uid: str, value: str,
include_snapshot: bool = False) -> bool:
"""
填写输入框
Args:
uid: 元素uid
value: 填写值
include_snapshot: 是否包含快照
Returns:
是否成功
"""
self.logger.debug(f"[MCP调用] fill: {uid} = {value}")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__fill(uid=uid, value=value, includeSnapshot=include_snapshot)
return True
def fill_form(self, elements: List[Dict],
include_snapshot: bool = False) -> bool:
"""
批量填写表单
Args:
elements: 元素列表,格式: [{"uid": "xxx", "value": "yyy"}]
include_snapshot: 是否包含快照
Returns:
是否成功
"""
self.logger.debug(f"[MCP调用] fill_form: {len(elements)} elements")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__fill_form(elements=elements, includeSnapshot=include_snapshot)
return True
def evaluate_script(self, function: str,
args: Optional[List[str]] = None) -> Any:
"""
执行JavaScript脚本
Args:
function: JavaScript函数
args: 参数列表(uid列表)
Returns:
脚本执行结果
"""
self.logger.debug(f"[MCP调用] evaluate_script: {function[:50]}...")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__evaluate_script(function=function, args=args or [])
return None
def get_attribute(self, uid: str, attribute: str) -> Optional[str]:
"""
获取元素属性
Args:
uid: 元素uid
attribute: 属性名
Returns:
属性值
"""
self.logger.debug(f"[MCP调用] get_attribute: {uid}.{attribute}")
# 使用evaluate_script获取属性
script = f"(el) => el.getAttribute('{attribute}')"
return self.evaluate_script(script, [uid])
def list_pages(self) -> List[Dict]:
"""
列出所有打开的页面
Returns:
页面列表
"""
self.logger.debug("[MCP调用] list_pages")
# 实际调用需要通过Claude Code的MCP工具
# result = mcp__chrome_devtools__list_pages()
return []
def close_page(self, page_id: int) -> bool:
"""
关闭指定页面
Args:
page_id: 页面ID
Returns:
是否成功
"""
self.logger.info(f"[MCP调用] close_page: {page_id}")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__close_page(pageId=page_id)
return True
def wait_for(self, text: List[str], timeout: int = 30000) -> bool:
"""
等待指定文本出现
Args:
text: 文本列表
timeout: 超时时间(毫秒)
Returns:
是否成功
"""
self.logger.debug(f"[MCP调用] wait_for: {text}")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__wait_for(text=text, timeout=timeout)
return True
def take_screenshot(self, file_path: Optional[str] = None,
format: str = "png", quality: int = 80,
full_page: bool = False) -> bool:
"""
截取屏幕截图
Args:
file_path: 保存路径
format: 图片格式(png/jpeg/webp)
quality: 图片质量(用于jpeg/webp)
full_page: 是否截取整页
Returns:
是否成功
"""
self.logger.debug("[MCP调用] take_screenshot")
# 实际调用需要通过Claude Code的MCP工具
# mcp__chrome_devtools__take_screenshot(filePath=file_path, format=format, quality=quality, fullPage=full_page)
return True
def get_console_messages(self, types: Optional[List[str]] = None,
page_idx: int = 0, page_size: int = 100) -> List[Dict]:
"""
获取控制台消息
Args:
types: 消息类型过滤
page_idx: 页面索引
page_size: 页面大小
Returns:
消息列表
"""
self.logger.debug("[MCP调用] get_console_messages")
# 实际调用需要通过Claude Code的MCP工具
# result = mcp__chrome_devtools__list_console_messages(types=types, pageIdx=page_idx, pageSize=page_size)
return []
# 全局MCP包装器实例(用于跟踪状态)
_mcp_wrapper_instance: Optional[MCPBrowserWrapper] = None
def get_mcp_wrapper() -> MCPBrowserWrapper:
"""获取全局MCP包装器实例"""
global _mcp_wrapper_instance
if _mcp_wrapper_instance is None:
_mcp_wrapper_instance = MCPBrowserWrapper()
return _mcp_wrapper_instance
# -*- coding: utf-8 -*-
"""
导航处理模块
负责处理两级菜单导航
"""
import time
from typing import Dict, Optional, Tuple
from browser_operator import BrowserOperator
from utils.logger import get_logger
from utils.constants import WAIT_SHORT, WAIT_MEDIUM
class NavigationHandler:
"""导航处理器 - 处理菜单导航"""
def __init__(self, browser: BrowserOperator):
"""
初始化导航处理器
Args:
browser: 浏览器操作器实例
"""
self.browser = browser
self.logger = get_logger("NavigationHandler")
def navigate_to_module(self, module_name: str, module_name_son: str) -> Tuple[bool, Optional[str]]:
"""
导航到指定模块(两级菜单)
Args:
module_name: 一级菜单名称
module_name_son: 二级菜单名称
Returns:
(是否成功, 当前路由)
"""
self.logger.info(f"开始导航到模块: {module_name} > {module_name_son}")
# 记录导航前的URL
before_url = self.browser.get_url()
# 1. 查找并展开一级菜单
if not self._expand_first_level_menu(module_name):
self.logger.error(f"展开一级菜单失败: {module_name}")
return False, None
time.sleep(WAIT_SHORT)
# 2. 查找并点击二级菜单
if not self._click_second_level_menu(module_name_son):
self.logger.error(f"点击二级菜单失败: {module_name_son}")
return False, None
# 3. 等待页面加载
time.sleep(WAIT_MEDIUM)
# 4. 获取当前路由
route = self._parse_current_route()
if route:
self.logger.info(f"导航成功,当前路由: {route}")
return True, route
else:
self.logger.warning("未能获取当前路由,但导航可能已成功")
return True, None
def _expand_first_level_menu(self, menu_name: str) -> bool:
"""
展开一级菜单
Args:
menu_name: 一级菜单名称
Returns:
是否成功展开
"""
self.logger.debug(f"正在查找一级菜单: {menu_name}")
snapshot = self.browser.take_snapshot()
# 查找一级菜单
menu_uid = self.browser.find_element_by_text(snapshot, menu_name)
if not menu_uid:
self.logger.error(f"未找到一级菜单: {menu_name}")
return False
self.logger.debug(f"找到一级菜单: {menu_name}, uid: {menu_uid}")
# 点击展开菜单
self.logger.info(f"正在点击一级菜单: {menu_name}")
if not self.browser.click_element(menu_uid):
self.logger.error(f"点击一级菜单失败: {menu_name}")
return False
return True
def _click_second_level_menu(self, submenu_name: str) -> bool:
"""
点击二级菜单
Args:
submenu_name: 二级菜单名称
Returns:
是否成功点击
"""
self.logger.debug(f"正在查找二级菜单: {submenu_name}")
snapshot = self.browser.take_snapshot()
# 查找二级菜单
submenu_uid = self.browser.find_element_by_text(snapshot, submenu_name)
if not submenu_uid:
self.logger.error(f"未找到二级菜单: {submenu_name}")
return False
self.logger.debug(f"找到二级菜单: {submenu_name}, uid: {submenu_uid}")
# 点击二级菜单
self.logger.info(f"正在点击二级菜单: {submenu_name}")
if not self.browser.click_element(submenu_uid):
self.logger.error(f"点击二级菜单失败: {submenu_name}")
return False
return True
def _parse_current_route(self) -> Optional[str]:
"""
解析当前页面的路由
Returns:
路由字符串,如果解析失败返回None
Note:
从URL中提取路由部分,例如:
URL: https://example.com/#/system/user
路由: system/user
"""
current_url = self.browser.get_url()
if not current_url:
self.logger.warning("当前URL为空")
return None
self.logger.debug(f"当前URL: {current_url}")
# 方法1:从hash中提取路由(/#/xxx格式)
if "#/" in current_url:
route = current_url.split("#/")[1]
# 去除查询参数
if "?" in route:
route = route.split("?")[0]
self.logger.debug(f"从hash中提取路由: {route}")
return route
# 方法2:从路径中提取路由
from urllib.parse import urlparse
parsed = urlparse(current_url)
path = parsed.path
# 去除开头的斜杠
if path.startswith("/"):
path = path[1:]
if path:
self.logger.debug(f"从路径中提取路由: {path}")
return path
self.logger.warning("未能从URL中提取路由")
return None
def find_menu_item(self, menu_text: str) -> Optional[str]:
"""
查找菜单项
Args:
menu_text: 菜单文本
Returns:
菜单项的uid,未找到返回None
"""
self.logger.debug(f"正在查找菜单项: {menu_text}")
snapshot = self.browser.take_snapshot()
uid = self.browser.find_element_by_text(snapshot, menu_text)
if uid:
self.logger.debug(f"找到菜单项: {menu_text}")
else:
self.logger.warning(f"未找到菜单项: {menu_text}")
return uid
def get_current_page_info(self) -> Dict:
"""
获取当前页面信息
Returns:
包含URL、路由等信息的字典
"""
current_url = self.browser.get_url()
route = self._parse_current_route()
return {
"url": current_url,
"route": route,
"title": self._get_page_title()
}
def _get_page_title(self) -> Optional[str]:
"""
获取页面标题
Returns:
页面标题
"""
try:
# 使用JavaScript获取页面标题
title = self.browser.execute_script("return document.title;")
return title
except Exception as e:
self.logger.error(f"获取页面标题失败: {e}")
return None
def wait_for_page_load(self, timeout: int = 30) -> bool:
"""
等待页面加载完成
Args:
timeout: 超时时间(秒)
Returns:
是否加载完成
"""
self.logger.debug("等待页面加载完成")
start_time = time.time()
while time.time() - start_time < timeout:
# 检查页面状态
ready_state = self.browser.execute_script("return document.readyState;")
if ready_state == "complete":
self.logger.debug("页面加载完成")
return True
time.sleep(0.5)
self.logger.warning("等待页面加载超时")
return False
def back(self) -> bool:
"""
返回上一页
Returns:
是否成功返回
"""
self.logger.info("正在返回上一页")
# 需要通过MCP工具实现
return True
def refresh(self) -> bool:
"""
刷新当前页面
Returns:
是否成功刷新
"""
self.logger.info("正在刷新页面")
# 需要通过MCP工具实现
return True
# -*- coding: utf-8 -*-
"""
操作执行模块
负责执行添加、编辑、删除操作,并记录操作步骤
"""
import time
from typing import Dict, List, Optional
from browser_operator import BrowserOperator
from element_locator import ElementLocator
from utils.logger import get_logger
from utils.constants import (
TEST_DATA,
EXPECTED_RESULTS,
WAIT_SHORT,
WAIT_MEDIUM
)
class OperationExecutor:
"""操作执行器 - 执行添加/编辑/删除操作"""
def __init__(self, browser: BrowserOperator, locator: ElementLocator):
"""
初始化操作执行器
Args:
browser: 浏览器操作器实例
locator: 元素定位器实例
"""
self.browser = browser
self.locator = locator
self.logger = get_logger("OperationExecutor")
self.test_data_created = False # 标记是否已创建测试数据
def execute_add(self, page_route: str) -> List[Dict]:
"""
执行添加操作
Args:
page_route: 当前页面路由
Returns:
操作步骤列表
"""
self.logger.info("开始执行添加操作")
steps = []
# 1. 点击添加按钮
step = self._click_operation_button("添加", page_route)
if step:
steps.append(step)
else:
self.logger.error("点击添加按钮失败")
return steps
time.sleep(WAIT_SHORT)
# 2. 等待表单弹窗
snapshot = self.browser.take_snapshot()
# 3. 收集并填写表单字段
form_steps = self._collect_and_fill_form(snapshot, page_route)
steps.extend(form_steps)
# 4. 点击确定按钮
step = self._click_confirm_button(page_route)
if step:
steps.append(step)
time.sleep(WAIT_SHORT)
# 5. 获取操作结果提示(弹窗提示)
step = self._get_operation_tips("添加", page_route)
if step:
steps.append(step)
# 6. 验证列表数据
step = self._verify_list_data("测试商品001", "添加", page_route)
if step:
steps.append(step)
self.logger.info(f"添加操作完成,共记录 {len(steps)} 个步骤")
return steps
def execute_edit(self, page_route: str) -> List[Dict]:
"""
执行编辑操作(创建数据→编辑→清理)
Args:
page_route: 当前页面路由
Returns:
操作步骤列表
"""
self.logger.info("开始执行编辑操作")
steps = []
# 1. 先创建测试数据
if not self.test_data_created:
self.logger.info("编辑操作需要先创建测试数据")
create_steps = self.execute_add(page_route)
steps.extend(create_steps)
self.test_data_created = True
time.sleep(WAIT_MEDIUM)
# 2. 点击编辑按钮
step = self._click_operation_button("编辑", page_route)
if step:
steps.append(step)
else:
self.logger.error("点击编辑按钮失败")
return steps
time.sleep(WAIT_SHORT)
# 3. 修改部分字段
snapshot = self.browser.take_snapshot()
edit_steps = self._modify_form_fields(snapshot, page_route)
steps.extend(edit_steps)
# 4. 点击确定按钮
step = self._click_confirm_button(page_route)
if step:
steps.append(step)
time.sleep(WAIT_SHORT)
# 5. 获取操作结果提示(弹窗提示)
step = self._get_operation_tips("编辑", page_route)
if step:
steps.append(step)
# 6. 验证列表数据
step = self._verify_list_data("测试商品002", "编辑", page_route)
if step:
steps.append(step)
# 7. 清理测试数据
self.logger.info("编辑操作完成,清理测试数据")
self._cleanup_test_data()
self.logger.info(f"编辑操作完成,共记录 {len(steps)} 个步骤")
return steps
def execute_delete(self, page_route: str) -> List[Dict]:
"""
执行删除操作(创建数据→删除→清理)
Args:
page_route: 当前页面路由
Returns:
操作步骤列表
"""
self.logger.info("开始执行删除操作")
steps = []
# 1. 先创建测试数据
if not self.test_data_created:
self.logger.info("删除操作需要先创建测试数据")
create_steps = self.execute_add(page_route)
steps.extend(create_steps)
self.test_data_created = True
time.sleep(WAIT_MEDIUM)
# 2. 点击删除按钮
step = self._click_operation_button("删除", page_route)
if step:
steps.append(step)
else:
self.logger.error("点击删除按钮失败")
return steps
time.sleep(WAIT_SHORT)
# 3. 确认删除
step = self._confirm_delete(page_route)
if step:
steps.append(step)
time.sleep(WAIT_SHORT)
# 4. 获取操作结果提示(弹窗提示)
step = self._get_operation_tips("删除", page_route)
if step:
steps.append(step)
# 5. 验证列表数据
step = self._verify_list_data("测试商品002", "删除", page_route)
if step:
steps.append(step)
# 6. 标记测试数据已清理
self.test_data_created = False
self.logger.info(f"删除操作完成,共记录 {len(steps)} 个步骤")
return steps
def _click_operation_button(self, operation: str, page_route: str) -> Optional[Dict]:
"""
点击操作按钮
Args:
operation: 操作类型(添加/编辑/删除)
page_route: 当前页面路由
Returns:
操作步骤字典,失败返回None
"""
self.logger.debug(f"正在查找{operation}按钮")
snapshot = self.browser.take_snapshot()
# 查找操作按钮
button_uid = self.browser.find_element_by_text(snapshot, operation)
if not button_uid:
# 尝试查找包含"新增"(添加的别称)
if operation == "添加":
button_uid = self.browser.find_element_by_text(snapshot, "新增")
if not button_uid:
self.logger.error(f"未找到{operation}按钮")
return None
# 分析元素定位
element_info = {"uid": button_uid, "text": operation}
locator_info = self.locator.locate_element(element_info)
# 点击按钮
if not self.browser.click_element(button_uid):
self.logger.error(f"点击{operation}按钮失败")
return None
# 生成操作步骤
step = self._generate_step(
page=page_route,
step=f"点击【{operation}】按钮",
locator_info=locator_info,
element_type="click",
element_value=""
)
return step
def _click_confirm_button(self, page_route: str) -> Optional[Dict]:
"""
点击确定按钮
Args:
page_route: 当前页面路由
Returns:
操作步骤字典,失败返回None
"""
self.logger.debug("正在查找确定按钮")
snapshot = self.browser.take_snapshot()
# 查找确定按钮(多种可能的文本)
confirm_keywords = ["确定", "提交", "保存", "OK"]
button_uid = None
button_text = None
for keyword in confirm_keywords:
button_uid = self.browser.find_element_by_text(snapshot, keyword)
if button_uid:
button_text = keyword
break
if not button_uid:
self.logger.error("未找到确定按钮")
return None
# 分析元素定位
element_info = {"uid": button_uid, "text": button_text}
locator_info = self.locator.locate_element(element_info)
# 点击按钮
if not self.browser.click_element(button_uid):
self.logger.error("点击确定按钮失败")
return None
# 生成操作步骤
step = self._generate_step(
page=page_route,
step=f"点击【{button_text}】按钮",
locator_info=locator_info,
element_type="click",
element_value=""
)
return step
def _collect_and_fill_form(self, snapshot: Dict, page_route: str) -> List[Dict]:
"""
收集并填写表单字段
Args:
snapshot: 页面快照
page_route: 当前页面路由
Returns:
操作步骤列表
"""
self.logger.debug("正在收集表单字段")
steps = []
# 常见的表单字段标识
field_keywords = {
"username": ["用户名", "账号", "用户"],
"password": ["密码", "新密码", "登录密码"],
"confirm_password": ["确认密码", "重复密码"],
"phone": ["手机", "电话", "手机号"],
"email": ["邮箱", "邮件"],
"name": ["姓名", "名称"],
"remark": ["备注", "说明"],
"code": ["编码", "代码"],
"description": ["描述", "简介"]
}
# 根据关键字查找并填写字段
for field_type, keywords in field_keywords.items():
for keyword in keywords:
field_uid = self.browser.find_element_by_text(snapshot, keyword)
if field_uid:
# 分析元素定位
element_info = {"uid": field_uid, "text": keyword}
locator_info = self.locator.locate_element(element_info)
# 确定元素类型
attributes = self.browser.get_element_attributes(field_uid)
element_type = self.locator.determine_element_type(attributes)
# 获取测试值
test_value = TEST_DATA.get(field_type, "test001")
# 填写字段
if element_type == "input":
self.browser.fill_input(field_uid, test_value)
# 生成操作步骤
step = self._generate_step(
page=page_route,
step=f"输入{keyword}",
locator_info=locator_info,
element_type=element_type,
element_value=test_value
)
steps.append(step)
break
self.logger.debug(f"收集到 {len(steps)} 个表单字段")
return steps
def _modify_form_fields(self, snapshot: Dict, page_route: str) -> List[Dict]:
"""
修改表单字段(用于编辑操作)
Args:
snapshot: 页面快照
page_route: 当前页面路由
Returns:
操作步骤列表
"""
self.logger.debug("正在修改表单字段")
steps = []
# 编辑操作只修改少量字段作为示例
# 这里修改用户名和备注字段
edit_fields = {
"username": "test002", # 修改后的用户名
"remark": "已编辑" # 修改后的备注
}
field_keywords = {
"username": ["用户名", "账号"],
"remark": ["备注"]
}
for field_type, keywords in field_keywords.items():
for keyword in keywords:
field_uid = self.browser.find_element_by_text(snapshot, keyword)
if field_uid:
# 分析元素定位
element_info = {"uid": field_uid, "text": keyword}
locator_info = self.locator.locate_element(element_info)
# 确定元素类型
attributes = self.browser.get_element_attributes(field_uid)
element_type = self.locator.determine_element_type(attributes)
# 获取修改后的值
edit_value = edit_fields.get(field_type, "test002")
# 清空并填写
self.browser.fill_input(field_uid, edit_value, clear_first=True)
# 生成操作步骤
step = self._generate_step(
page=page_route,
step=f"修改{keyword}",
locator_info=locator_info,
element_type=element_type,
element_value=edit_value
)
steps.append(step)
break
return steps
def _confirm_delete(self, page_route: str) -> Optional[Dict]:
"""
确认删除操作
Args:
page_route: 当前页面路由
Returns:
操作步骤字典,失败返回None
"""
self.logger.debug("正在确认删除")
snapshot = self.browser.take_snapshot()
# 查找确认按钮
confirm_keywords = ["确定", "确认", "是"]
button_uid = None
button_text = None
for keyword in confirm_keywords:
button_uid = self.browser.find_element_by_text(snapshot, keyword)
if button_uid:
button_text = keyword
break
if not button_uid:
self.logger.warning("未找到确认删除按钮,可能不需要确认")
return None
# 分析元素定位
element_info = {"uid": button_uid, "text": button_text}
locator_info = self.locator.locate_element(element_info)
# 点击确认
if not self.browser.click_element(button_uid):
self.logger.error("确认删除失败")
return None
# 生成操作步骤
step = self._generate_step(
page=page_route,
step=f"点击确认删除按钮",
locator_info=locator_info,
element_type="click",
element_value=""
)
return step
def _get_operation_tips(self, operation: str, page_route: str) -> Optional[Dict]:
"""
获取操作结果提示(弹窗提示信息)
Args:
operation: 操作类型
page_route: 当前页面路由
Returns:
操作步骤字典
"""
self.logger.debug(f"正在获取{operation}操作提示")
snapshot = self.browser.take_snapshot()
# 查找提示信息元素(Element UI的el-message组件)
tip_keywords = ["成功", "失败", "错误", "提示"]
# 获取预期结果
expected_result = EXPECTED_RESULTS.get(operation, f"{operation}成功")
# 查找提示元素
tip_uid = None
for keyword in tip_keywords:
tip_uid = self.browser.find_element_by_text(snapshot, keyword)
if tip_uid:
break
# 即使没找到提示元素,也生成步骤
if tip_uid:
element_info = {"uid": tip_uid, "text": "提示信息"}
locator_info = self.locator.locate_element(element_info)
else:
# 使用默认的提示元素定位器
locator_info = {
"locator_type": "XPATH",
"locator_value": "//p[@class='el-message__content']"
}
# 生成操作步骤 - getTips类型的element_value留空,预期结果填写在expected_result
step = self._generate_step(
page=page_route,
step=f"获取操作提示",
locator_info=locator_info,
element_type="getTips",
element_value="",
expected_result=expected_result
)
return step
def _verify_list_data(self, data_text: str, operation: str, page_route: str) -> Optional[Dict]:
"""
验证列表数据(使用getText获取列表中的文本)
Args:
data_text: 要查找的数据文本
operation: 操作类型
page_route: 当前页面路由
Returns:
操作步骤字典
"""
self.logger.debug(f"正在验证列表数据: {data_text}")
# 查找列表中的数据(通常在td标签中)
# 使用通用的XPath定位列表数据
locator_value = f"//td[contains(.,'{data_text}')]"
locator_type = "XPATH"
# 生成预期结果描述
expected_result = f"列表中{'包含' if operation != '删除' else '不包含'}{operation}后的数据"
# 生成操作步骤 - getText类型的element_value留空,预期结果填写在expected_result
step = self._generate_step(
page=page_route,
step=f"验证列表数据",
locator_info={
"locator_type": locator_type,
"locator_value": locator_value
},
element_type="getText",
element_value="",
expected_result=expected_result
)
return step
def _cleanup_test_data(self) -> None:
"""清理测试数据"""
self.logger.info("清理测试数据")
self.test_data_created = False
def _generate_step(self, page: str, step: str, locator_info: Dict,
element_type: str, element_value: str,
expected_result: str = "") -> Dict:
"""
生成操作步骤字典
Args:
page: 页面路由
step: 步骤描述
locator_info: 定位信息
element_type: 元素类型
element_value: 元素值
expected_result: 预期结果
Returns:
操作步骤字典
"""
return {
"page": page,
"step": step,
"locator_type": locator_info.get("locator_type", "XPATH"),
"locator_value": locator_info.get("locator_value", ""),
"element_type": element_type,
"element_value": element_value,
"expected_result": expected_result
}
# -*- coding: utf-8 -*-
"""
MCP功能测试脚本
验证chrome-devtools MCP工具的基本功能
"""
import json
import time
from pathlib import Path
def test_mcp_basic():
"""测试MCP基本功能"""
print("=" * 50)
print("MCP基本功能测试")
print("=" * 50)
results = []
# 测试1: 列出当前页面
print("\n[测试1] 列出当前页面")
try:
pages = mcp__chrome_devtools__list_pages()
print(f"✓ 成功获取页面列表: {len(pages.get('pages', []))} 个页面")
if pages.get('pages'):
print(f" 当前页面: {pages['pages'][0]}")
results.append(("list_pages", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("list_pages", False, str(e)))
# 测试2: 导航到测试页面
print("\n[测试2] 导航到测试页面")
test_url = "https://www.baidu.com"
try:
result = mcp__chrome_devtools__navigate_page(type="url", url=test_url)
time.sleep(2) # 等待页面加载
print(f"✓ 成功导航到: {test_url}")
results.append(("navigate", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("navigate", False, str(e)))
# 测试3: 获取页面快照
print("\n[测试3] 获取页面快照")
try:
snapshot = mcp__chrome_devtools__take_snapshot()
snapshot_text = str(snapshot)[:200]
print(f"✓ 成功获取快照")
print(f" 快照预览: {snapshot_text}...")
results.append(("take_snapshot", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("take_snapshot", False, str(e)))
# 测试4: 查找搜索框元素
print("\n[测试4] 查找搜索框元素")
try:
snapshot = mcp__chrome_devtools__take_snapshot()
# 百度搜索框的文本
search_keywords = ["百度", "搜索", "百度一下"]
found_uid = None
for keyword in search_keywords:
if keyword in str(snapshot):
print(f"✓ 在快照中找到关键词: {keyword}")
found_uid = "search_box" # 模拟uid
break
if found_uid:
results.append(("find_element", True, f"找到元素: {found_uid}"))
else:
print(f"⚠ 未在快照中找到搜索关键词")
results.append(("find_element", False, "未找到搜索框"))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("find_element", False, str(e)))
# 测试5: 执行JavaScript脚本
print("\n[测试5] 执行JavaScript脚本")
try:
script = "() => { return document.title; }"
result = mcp__chrome_devtools__evaluate_script(function=script)
print(f"✓ 成功执行脚本")
print(f" 页面标题: {result}")
results.append(("evaluate_script", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("evaluate_script", False, str(e)))
# 测试6: 获取当前URL
print("\n[测试6] 获取当前URL")
try:
script = "() => { return window.location.href; }"
result = mcp__chrome_devtools__evaluate_script(function=script)
print(f"✓ 成功获取当前URL: {result}")
results.append(("get_url", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("get_url", False, str(e)))
# 测试7: 截取屏幕截图
print("\n[测试7] 截取屏幕截图")
try:
screenshot_path = "E:/GithubData/ubains-module-test/AuxiliaryTool/TestCaseGenerator/test_screenshot.png"
result = mcp__chrome_devtools__take_screenshot(filePath=screenshot_path, format="png")
print(f"✓ 成功截取屏幕截图")
print(f" 保存路径: {screenshot_path}")
results.append(("take_screenshot", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("take_screenshot", False, str(e)))
# 打印测试结果汇总
print("\n" + "=" * 50)
print("测试结果汇总")
print("=" * 50)
passed = sum(1 for _, success, _ in results if success)
total = len(results)
for test_name, success, error in results:
status = "✓ 通过" if success else "✗ 失败"
print(f"{status:10} {test_name:20} {error if error else ''}")
print(f"\n总计: {passed}/{total} 通过")
return results
def test_mcp_form_interaction():
"""测试MCP表单交互功能"""
print("\n" + "=" * 50)
print("MCP表单交互测试")
print("=" * 50)
results = []
# 导航到百度
print("\n[步骤1] 导航到百度")
try:
mcp__chrome_devtools__navigate_page(type="url", url="https://www.baidu.com")
time.sleep(2)
print("✓ 成功导航到百度")
results.append(("navigate_baidu", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("navigate_baidu", False, str(e)))
return results
# 获取快照并分析
print("\n[步骤2] 获取页面快照")
try:
snapshot = mcp__chrome_devtools__take_snapshot()
print("✓ 成功获取快照")
# 分析快照结构
snapshot_str = str(snapshot)
print(f" 快照长度: {len(snapshot_str)} 字符")
# 查找输入框相关内容
if "input" in snapshot_str.lower() or "textbox" in snapshot_str.lower():
print(" ✓ 快照中包含输入框元素")
results.append(("analyze_snapshot", True, "找到输入框"))
else:
print(" ⚠ 快照中未找到明显的输入框元素")
results.append(("analyze_snapshot", False, "未找到输入框"))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("analyze_snapshot", False, str(e)))
# 尝试填写搜索框(使用JavaScript)
print("\n[步骤3] 尝试填写搜索框")
try:
# 百度搜索框的ID通常是kw
script = """(text) => {
const input = document.getElementById('kw');
if (input) {
input.value = text;
return {success: true, element: 'input#kw'};
}
return {success: false, error: 'input#kw not found'};
}"""
result = mcp__chrome_devtools__evaluate_script(function=script, args=["自动化测试"])
print(f"✓ 成功执行填写脚本")
print(f" 结果: {result}")
results.append(("fill_input", True, str(result)))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("fill_input", False, str(e)))
# 截取填写后的截图
print("\n[步骤4] 截取填写后的页面")
try:
screenshot_path = "E:/GithubData/ubains-module-test/AuxiliaryTool/TestCaseGenerator/test_filled.png"
mcp__chrome_devtools__take_screenshot(filePath=screenshot_path, format="png")
print(f"✓ 成功截取截图: {screenshot_path}")
results.append(("screenshot_filled", True, ""))
except Exception as e:
print(f"✗ 失败: {e}")
results.append(("screenshot_filled", False, str(e)))
return results
def save_test_results(results: list, test_name: str):
"""保存测试结果到文件"""
output_dir = Path("E:/GithubData/ubains-module-test/AuxiliaryTool/TestCaseGenerator/test_results")
output_dir.mkdir(exist_ok=True)
timestamp = time.strftime("%Y%m%d_%H%M%S")
result_file = output_dir / f"{test_name}_{timestamp}.json"
summary = {
"test_name": test_name,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"total_tests": len(results),
"passed_tests": sum(1 for _, success, _ in results if success),
"results": [
{
"test": name,
"success": success,
"error": error
}
for name, success, error in results
]
}
with open(result_file, 'w', encoding='utf-8') as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f"\n测试结果已保存到: {result_file}")
if __name__ == "__main__":
print("开始MCP功能测试...")
# 基本功能测试
basic_results = test_mcp_basic()
save_test_results(basic_results, "mcp_basic_test")
# 表单交互测试
form_results = test_mcp_form_interaction()
save_test_results(form_results, "mcp_form_test")
print("\n" + "=" * 50)
print("所有测试完成!")
print("=" * 50)
# -*- coding: utf-8 -*-
"""
测试用例生成模块
负责根据操作步骤生成JSON格式的测试用例文件
"""
import json
from pathlib import Path
from typing import Dict, List
from datetime import datetime
from utils.logger import get_logger
from utils.constants import EXPECTED_RESULTS
class TestCaseGenerator:
"""测试用例生成器"""
def __init__(self, module_info: Dict, base_url: str):
"""
初始化测试用例生成器
Args:
module_info: 模块信息字典
base_url: 系统基础URL
"""
self.module_info = module_info
self.base_url = base_url
self.logger = get_logger("TestCaseGenerator")
self.module_name = module_info.get("module_name", "")
self.module_name_son = module_info.get("module_name_son", "")
def generate_testcase(self, operation: str, steps: List[Dict]) -> Dict:
"""
生成测试用例
Args:
operation: 操作类型(添加/编辑/删除)
steps: 操作步骤列表
Returns:
测试用例字典
"""
self.logger.info(f"正在生成测试用例: {self.generate_name(operation)}")
testcase = {
"name": self.generate_name(operation),
"para": steps,
"platform": "web",
"base_url": self.base_url
}
# 验证测试用例
if not self._validate_testcase(testcase):
self.logger.error("测试用例验证失败")
return {}
self.logger.info(f"测试用例生成成功,共 {len(steps)} 个步骤")
return testcase
def generate_name(self, operation: str) -> str:
"""
生成测试用例名称
Args:
operation: 操作类型
Returns:
测试用例名称
"""
return f"{self.module_name}_{self.module_name_son}_{operation}"
def generate_expected_result(self, operation: str) -> str:
"""
生成预期结果
Args:
operation: 操作类型
Returns:
预期结果字符串
"""
return EXPECTED_RESULTS.get(operation, f"{operation}成功")
def save_testcase(self, testcase: Dict, output_dir: Path) -> str:
"""
保存测试用例到JSON文件
Args:
testcase: 测试用例字典
output_dir: 输出目录
Returns:
保存的文件路径
"""
if not testcase:
self.logger.error("测试用例为空,无法保存")
return ""
# 确保输出目录存在
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 生成文件名
filename = f"{testcase['name']}.json"
filepath = output_dir / filename
self.logger.info(f"正在保存测试用例: {filepath}")
try:
# 格式化JSON并保存
json_content = self.format_json(testcase)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(json_content)
self.logger.info(f"测试用例保存成功: {filepath}")
return str(filepath)
except Exception as e:
self.logger.error(f"保存测试用例失败: {e}")
return ""
def format_json(self, data: Dict) -> str:
"""
格式化JSON输出
Args:
data: 要格式化的数据
Returns:
格式化后的JSON字符串
"""
return json.dumps(
data,
ensure_ascii=False,
indent=2,
separators=(',', ': ')
)
def _validate_testcase(self, testcase: Dict) -> bool:
"""
验证测试用例格式
Args:
testcase: 测试用例字典
Returns:
是否验证通过
"""
# 检查必填字段
required_fields = ["name", "para", "platform", "base_url"]
for field in required_fields:
if field not in testcase:
self.logger.error(f"测试用例缺少必填字段: {field}")
return False
# 检查para是否为列表
if not isinstance(testcase["para"], list):
self.logger.error("测试用例的para字段必须是列表")
return False
# 检查每个步骤的必填字段
step_required_fields = ["page", "step", "locator_type", "locator_value", "element_type"]
for idx, step in enumerate(testcase["para"]):
for field in step_required_fields:
if field not in step:
self.logger.warning(f"步骤{idx+1}缺少字段: {field}")
return True
def generate_summary(self, testcases: List[Dict]) -> Dict:
"""
生成测试用例汇总信息
Args:
testcases: 测试用例列表
Returns:
汇总信息字典
"""
summary = {
"module_name": self.module_name,
"module_name_son": self.module_name_son,
"total_count": len(testcases),
"testcases": [tc.get("name", "") for tc in testcases],
"generated_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
return summary
def save_summary(self, summary: Dict, output_dir: Path) -> str:
"""
保存汇总信息
Args:
summary: 汇总信息字典
output_dir: 输出目录
Returns:
保存的文件路径
"""
filename = f"{self.module_name}_{self.module_name_son}_summary.json"
filepath = Path(output_dir) / filename
try:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(self.format_json(summary))
self.logger.info(f"汇总信息保存成功: {filepath}")
return str(filepath)
except Exception as e:
self.logger.error(f"保存汇总信息失败: {e}")
return ""
def batch_generate(self, operations: List[str], steps_dict: Dict[str, List[Dict]],
output_dir: Path) -> List[str]:
"""
批量生成测试用例
Args:
operations: 操作类型列表
steps_dict: 操作步骤字典,key为操作类型,value为步骤列表
output_dir: 输出目录
Returns:
生成的文件路径列表
"""
self.logger.info(f"开始批量生成测试用例,共 {len(operations)} 个操作")
testcases = []
filepaths = []
for operation in operations:
steps = steps_dict.get(operation, [])
if not steps:
self.logger.warning(f"操作 {operation} 没有步骤,跳过")
continue
# 生成测试用例
testcase = self.generate_testcase(operation, steps)
if testcase:
testcases.append(testcase)
# 保存测试用例
filepath = self.save_testcase(testcase, output_dir)
if filepath:
filepaths.append(filepath)
# 生成并保存汇总信息
if testcases:
summary = self.generate_summary(testcases)
self.save_summary(summary, output_dir)
self.logger.info(f"批量生成完成,共生成 {len(filepaths)} 个文件")
return filepaths
# -*- coding: utf-8 -*-
"""
工具函数模块
提供日志、常量等公共功能
"""
from .logger import get_logger
from .constants import *
__all__ = ['get_logger']
# -*- coding: utf-8 -*-
"""
常量定义模块
定义系统中使用的各种常量
"""
# 固定测试数据
TEST_DATA = {
"username": "test001",
"password": "Test@123456",
"phone": "13800138000",
"email": "test001@example.com",
"name": "测试用户",
"description": "测试内容",
"remark": "自动化测试",
"code": "001"
}
# 预期结果映射
EXPECTED_RESULTS = {
"添加": "添加成功",
"编辑": "编辑成功",
"删除": "删除成功",
"提交": "提交成功",
"保存": "保存成功",
"修改": "修改成功"
}
# 支持的元素类型
ELEMENT_TYPES = {
# 基础类型
"click": "点击按钮、链接等可点击元素",
"input": "文本输入框",
"select": "下拉选择框",
# 选择类型
"checkbox": "复选框/单选框",
"switch": "开关控件",
# 验证类型
"getTips": "获取弹窗提示信息(如Element UI的el-message组件)",
"getText": "获取列表数据文本(如表格td元素中的文本)"
}
# element_type 与 element_value 填写规则映射
ELEMENT_VALUE_RULES = {
"input": "填写需输入的文本内容",
"getTips": "留空(验证内容填写在expected_result中)",
"getText": "留空(验证内容填写在expected_result中)",
"click": "留空",
"select": "留空",
"checkbox": "留空",
"switch": "留空"
}
# 定位策略优先级(注意:不允许使用UID)
LOCATOR_PRIORITIES = [
"ID", # 优先级1:ID属性
"NAME", # 优先级2:Name属性
"CLASS", # 优先级3:Class属性
"XPATH", # 备选:XPATH表达式
"CSS_SELECTOR" # 备选:CSS选择器
]
# 禁止使用的定位类型
FORBIDDEN_LOCATOR_TYPES = ["UID"]
# XPath规范
XPATH_CONTAINS_PATTERN = "contains(.,'{text}')" # 使用 . 而非 text()
XPATH_CONTAINS_OLD_PATTERN = "contains(text(),'{text}')" # 旧的错误模式
# 定位策略优先级
LOCATOR_PRIORITIES = [
"ID", # 优先级1:ID属性
"NAME", # 优先级2:Name属性
"CLASS", # 优先级3:Class属性
"XPATH", # 备选:XPATH表达式
"CSS_SELECTOR" # 备选:CSS选择器
]
# 等待超时时间(秒)
WAIT_TIMEOUT = 30
WAIT_SHORT = 5
WAIT_MEDIUM = 10
# MCP相关常量
MCP_TIMEOUT = 60000 # MCP操作超时时间(毫秒)
# 浏览器相关
BROWSER_WIDTH = 1920
BROWSER_HEIGHT = 1080
# 文件相关
CONFIG_DIR = "config"
TESTCASES_DIR = "testcases"
LOGS_DIR = "logs"
# -*- coding: utf-8 -*-
"""
日志工具模块
提供统一的日志输出功能
"""
import logging
import sys
from pathlib import Path
from datetime import datetime
def get_logger(name: str = "TestCaseGenerator", level: int = logging.INFO) -> logging.Logger:
"""
获取日志记录器
Args:
name: 日志记录器名称
level: 日志级别
Returns:
配置好的日志记录器
"""
logger = logging.getLogger(name)
# 避免重复添加handler
if logger.handlers:
return logger
logger.setLevel(level)
# 控制台输出handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
# 日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
return logger
def setup_file_logger(logger: logging.Logger, log_dir: Path) -> None:
"""
设置文件日志输出
Args:
logger: 日志记录器
log_dir: 日志文件目录
"""
log_dir.mkdir(parents=True, exist_ok=True)
# 日志文件名:按日期命名
log_file = log_dir / f"testcase_generator_{datetime.now().strftime('%Y%m%d')}.log"
# 文件输出handler
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
# 文件日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# 需求文档
## 需求背景与目标
### 1.1 背景
当前自动化测试用例仍是手动编写,效率较低,且无法批量生成,且无法自动生成JSON数据模板,无法自动生成测试用例。需要实现自动化测试用例生成。
### 1.2 目标
通过claude code+mcp,根据system_config.json获取系统登录地址、账号密码。根据提供的module_config.json自动访问系统的对应模块进行操作,收集操作步骤的元素定位类型以及元素定位值,并通过自动化测试用例JSON数据模板输出自动化测试用例。实现快速自动补充JSON数据
## 2. 功能需求
1. 根据同目录下的config.json文件,获取系统登录地址、账号密码。
- system_config.json文件格式示例:
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
},
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
```
2. 根据同目录下的module_config.json文件,获取需要收集的自动化模块名称、模块功能。
- module_config.json文件格式示例:
```json
[
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
},
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
}
]
```
3. 通过claude code+mcp,自动访问系统进行模块操作,并通过自动化测试用例JSON数据模板输出自动化测试用例。
- 自动化测试用例JSON数据模板格式示例:
```json
{
"name": "{module_name}_{module_name_son}_{model_function}",
"para": [
{
"page": "login/logindf",
"step": "点击左侧【用户管理】",
"locator_type": "XPATH",
"locator_value": "//span[contains(.,'用户管理')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "点击【用户列表】按钮",
"locator_type": "XPATH",
"locator_value": "//li[contains(.,'用户列表')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "点击【新增】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(.,'添加')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入账号",
"locator_type": "XPATH",
"locator_value": "//input[@id='accountChange']",
"element_type": "input",
"element_value": "admin@zdh",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入用户名",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='用户名']",
"element_type": "input",
"element_value": "admin@zdh",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入新密码",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='11位及以上的大小写字母和数字且连续3位及以上不重复和不连续组合']",
"element_type": "input",
"element_value": "Ubains@1357",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入确认密码",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='确认密码']",
"element_type": "input",
"element_value": "Ubains@1357",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "点击【确定】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(.,'确定')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "获取提示文本",
"locator_type": "XPATH",
"locator_value": "//p[@class='el-message__content']",
"element_type": "getTips",
"element_value": "",
"expected_result": "添加成功"
},
{
"page": "login/logindf",
"step": "验证列表数据",
"locator_type": "XPATH",
"locator_value": "//td[contains(.,'admin@zdh')]",
"element_type": "getText",
"element_value": "",
"expected_result": "列表中包含新添加的用户"
}
],
"platform": "web",
"base_url": "https://192.168.5.44/"
}
```
- 参数说明:
- name: 测试用例名称,命名格式为:{module_name}_{module_name_son}_{model_function},例如:区域管理_增值服务_添加。
- para: 测试步骤列表,每个步骤包含以下字段:
- page: page字段为当前模块功能操作的页面路由后缀。
- step: 操作步骤描述,例如:点击左侧【用户管理】。
- locator_type: 元素定位类型,例如:ID、XPATH、CSS_SELECTOR等。优先获取ID、XPATH,**不允许用UID**。
- locator_value: 元素定位值,例如://button[contains(.,'确定')],需要与locator_type一致。
- **注意**:XPATH中使用 `contains(.,'文本')` 而非 `contains(text(),'文本')`,以正确匹配Element UI等框架中span子元素内的文本。
- element_type: 操作类型,例如:click、input、select、checkbox、switch、getTips、getText。
- element_value: 操作值,**根据element_type不同有不同填写规则**:
- `input`:填写需输入的文本内容(如"test001"
- `getTips`:**留空**,预期结果填写在expected_result字段
- `getText`:**留空**,预期结果填写在expected_result字段
- `click`、`select`、`checkbox`、`switch`:**留空**
- expected_result: 预期结果描述,根据操作类型填写,如"添加成功""列表中包含新添加的数据"等。
4. 输出的自动化测试用例文件命名格式为:{module_name}_{module_name_son}_{model_function}.json,例如:区域管理_增值服务_添加.json。
- 模板参考:[自动化测试用例模板](新统一平台/测试数据/新统一平台测试用例 - 副本.xlsx)
- 输出位置:config文件同目录下的 `testcases/` 子目录
## 3. 需求补充说明
### 3.1 配置文件格式规范
#### 3.1.1 system_config.json格式
采用标准JSON格式,包含以下字段:
- `system_type`: 系统类型标识
- `system_front_url`: 系统前台地址
- `system_back_url`: 系统后台地址
- `username`: 登录账号
- `password`: 登录密码
- `code`: 验证码(固定值:csba)
**注意**:登录时需用户输入选择使用前台或后台URL。
#### 3.1.2 module_config.json格式
- `module_name`: 一级菜单名称
- `module_name_son`: 二级菜单名称
- `module_function`: 功能列表,支持"添加""编辑""删除"
### 3.2 执行流程规范
#### 3.2.1 模块导航方式
采用**两级菜单导航**:
1. 先点击一级菜单(`module_name`)
2. 再点击二级菜单(`module_name_son`)
#### 3.2.2 功能操作策略
- **添加操作**:填写通用固定测试值
- **编辑操作**:先自动创建测试数据 执行编辑 清理测试数据
- **删除操作**:先自动创建测试数据 执行删除 清理测试数据
#### 3.2.3 元素定位策略
采用**智能检测**方式,按以下优先级选择定位方式:
1. **优先级1**:id属性(唯一标识)
2. **优先级2**:name属性
3. **优先级3**:class属性
4. **备选方案**:XPATH表达式
#### 3.2.4 Page字段获取
通过**URL解析**获取当前功能所在页面的路由后缀。
#### 3.2.5 预期结果生成
根据操作类型**自动生成**预期结果提示文本,例如:
- 添加操作 "添加成功"
- 编辑操作 "编辑成功"
- 删除操作 "删除成功"
### 3.3 支持的元素类型(element_type)
#### 3.3.1 基础类型
- **click**:点击按钮、链接等可点击元素
- **input**:文本输入框(input type=text)
- **select**:下拉选择框(select)
#### 3.3.2 选择类型
- **checkbox**:复选框(checkbox)和单选框(radio)
- **switch**:开关控件(如Element UI的switch)
#### 3.3.3 验证类型
- **getTips**:获取弹窗提示信息(如Element UI的el-message组件中的"添加成功""删除成功"等提示)
- **getText**:获取列表中的文本数据(如表格td元素中的文本,用于验证数据是否存在于列表中)
### 3.4 element_value expected_result 填写规范
#### 3.4.1 element_value 填写规则
| element_type | element_value 填写内容 | 示例 |
|-------------|----------------------|------|
| `input` | 需输入的文本内容 | `"test001"` |
| `getTips` | **留空** | `""` |
| `getText` | **留空** | `""` |
| `click` | **留空** | `""` |
| `select` | **留空** | `""` |
| `checkbox` | **留空** | `""` |
| `switch` | **留空** | `""` |
#### 3.4.2 expected_result 填写规则
| element_type | expected_result 填写内容 | 示例 |
|-------------|-------------------------|------|
| `input` | 通常留空(非验证步骤) | `""` |
| `click` | 通常留空(非验证步骤) | `""` |
| `select` | 通常留空(非验证步骤) | `""` |
| `getTips` | 预期的弹窗提示文本 | `"添加成功"` |
| `getText` | 预期的验证描述 | `"列表中包含新添加的商品"` |
#### 3.4.3 完整示例对比
**错误的写法**(element_value填写了验证相关内容):
```json
{
"element_type": "getTips",
"element_value": "添加成功", // ❌ 错误
"expected_result": ""
}
```
**正确的写法**(验证内容填写在expected_result):
```json
{
"element_type": "getTips",
"element_value": "", // ✅ 正确:留空
"expected_result": "添加成功" // ✅ 正确:填写预期结果
}
```
### 3.5 测试数据规范
#### 3.4.1 固定测试值
使用**通用测试值**,例如:
- 用户名:test001
- 手机号:13800138000
- 密码:Test@123456
- 邮箱:test001@example.com
### 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
*文档结束*
\ No newline at end of file
# _PRD_自动化测试用例生成需求文档_计划执行
> 版本:V1.0
> 创建日期:2026-03-06
> 更新日期:2026-03-06
> 适用范围:自动化测试用例生成工具
> 来源:基于《_PRD_自动化测试用例生成需求文档.md》
> 状态:待执行
---
## 1. 项目概述
### 1.1 背景
当前自动化测试用例仍是手动编写,效率较低,且无法批量生成JSON数据模板。需要通过Claude Code + MCP技术,实现自动化测试用例的自动生成,提高测试用例编写效率。
### 1.2 目标
- 使用chrome-devtools MCP工具自动访问系统
- 根据配置文件自动执行模块操作并收集元素定位信息
- 按照JSON模板自动生成测试用例文件
- 支持添加、编辑、删除三种操作的自动化生成
- 实现快速批量生成测试用例
### 1.3 技术栈
- **Claude Code**: AI代码生成与执行
- **chrome-devtools MCP**: 浏览器自动化工具
- **Python**: 主要开发语言
- **JSON**: 配置文件和输出格式
### 1.4 涉及文件
**输入文件:**
- `system_config.json` - 系统配置文件(登录信息)
- `module_config.json` - 模块配置文件(测试模块和功能)
**输出文件:**
- `{module_name}_{module_name_son}_{function}.json` - 生成的测试用例文件
- 输出目录:config文件同目录下的 `testcases/` 子目录
**需要创建的模块文件:**
| 序号 | 模块名称 | 文件名 | 职责 | 状态 |
|-----|---------|--------|------|------|
| 1 | 配置管理模块 | config_manager.py | 读取和解析配置文件 | ⏳ 待创建 |
| 2 | 浏览器操作模块 | browser_operator.py | 封装chrome-devtools MCP操作 | ⏳ 待创建 |
| 3 | 登录模块 | login_handler.py | 处理系统登录 | ⏳ 待创建 |
| 4 | 导航模块 | navigation_handler.py | 处理菜单导航 | ⏳ 待创建 |
| 5 | 元素定位模块 | element_locator.py | 智能检测元素定位方式 | ⏳ 待创建 |
| 6 | 操作执行模块 | operation_executor.py | 执行添加/编辑/删除操作 | ⏳ 待创建 |
| 7 | 用例生成模块 | testcase_generator.py | 生成JSON测试用例 | ⏳ 待创建 |
| 8 | 主控模块 | main.py | 统一调度各模块 | ⏳ 待创建 |
---
## 2. 功能需求分析
### 2.1 核心功能流程
```
主程序启动
├── 1) 读取system_config.json获取系统信息
├── 2) 读取module_config.json获取测试模块
├── 3) 用户选择使用前台或后台
├── 4) 初始化chrome-devtools MCP连接
├── 5) 登录系统(固定验证码csba)
├── 6) 遍历module_config中的模块配置
│ ├── 6.1) 两级菜单导航(module_name → module_name_son)
│ ├── 6.2) 执行功能操作
│ │ ├── 添加操作:填写固定测试值
│ │ ├── 编辑操作:创建→编辑→清理
│ │ └── 删除操作:创建→删除→清理
│ ├── 6.3) 智能检测元素定位
│ ├── 6.4) 从URL解析page字段
│ ├── 6.5) 自动生成expected_result
│ └── 6.6) 生成JSON测试用例文件
└── 7) 输出到testcases/目录
```
### 2.2 配置文件规范
#### 2.2.1 system_config.json格式
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
```
#### 2.2.2 module_config.json格式
```json
[
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加", "编辑", "删除"]
}
]
```
### 2.3 测试用例JSON模板
```json
{
"name": "{module_name}_{module_name_son}_{function}",
"para": [
{
"page": "页面路由",
"step": "操作步骤描述",
"locator_type": "ID/XPATH/CSS_SELECTOR",
"locator_value": "元素定位值",
"element_type": "click/input/select/checkbox/switch/getTips/getText",
"element_value": "操作值",
"expected_result": "预期结果"
}
],
"platform": "web",
"base_url": "系统URL"
}
```
**参数填写规范:**
| 参数 | 说明 | 填写规则 |
|------|------|----------|
| `locator_type` | 元素定位类型 | 优先使用ID、XPATH;**不允许使用UID** |
| `locator_value` | 元素定位值 | XPATH使用 `contains(.,'text')` 而非 `contains(text(),'text')` |
| `element_type` | 操作类型 | click/input/select/checkbox/switch/getTips/getText |
| `element_value` | 操作值 | 根据element_type填写(详见下文规范) |
| `expected_result` | 预期结果 | 根据element_type填写(详见下文规范) |
---
## 3. 模块详细设计
### 3.1 config_manager.py - 配置管理模块
**职责:**
- 读取system_config.json文件
- 读取module_config.json文件
- 验证配置文件格式
- 提供配置数据访问接口
**导出函数:**
```python
def load_system_config(config_path: str) -> List[Dict]
def load_module_config(config_path: str) -> List[Dict]
def validate_system_config(config: Dict) -> bool
def validate_module_config(config: Dict) -> bool
```
**依赖:**
- Python标准库:json、pathlib
---
### 3.2 browser_operator.py - 浏览器操作模块
**职责:**
- 封装chrome-devtools MCP工具调用
- 提供统一的浏览器操作接口
- 处理页面快照和元素查找
- 错误处理和重试机制
**导出函数:**
```python
class BrowserOperator:
def __init__(self)
def open_page(self, url: str) -> bool
def take_snapshot(self) -> Dict
def find_element(self, text: str) -> str
def click_element(self, uid: str) -> bool
def fill_input(self, uid: str, value: str) -> bool
def select_option(self, uid: str, value: str) -> bool
def get_url(self) -> str
def wait_for_element(self, text: str, timeout: int) -> bool
```
**依赖:**
- chrome-devtools MCP工具
- mcp__chrome_devtools_* 系列函数
---
### 3.3 login_handler.py - 登录模块
**职责:**
- 处理系统登录流程
- 填写用户名、密码、验证码
- 处理登录后的页面状态
- 验证登录成功
**导出函数:**
```python
class LoginHandler:
def __init__(self, browser: BrowserOperator)
def login(self, url: str, username: str, password: str, code: str) -> bool
def verify_login_success(self) -> bool
```
**登录流程:**
1. 打开登录页面
2. 查找用户名输入框并填写
3. 查找密码输入框并填写
4. 查找验证码输入框并填写(固定csba)
5. 查找登录按钮并点击
6. 等待页面跳转
7. 验证登录成功
**依赖:**
- browser_operator模块
---
### 3.4 navigation_handler.py - 导航模块
**职责:**
- 处理两级菜单导航
- 根据module_name和module_name_son定位菜单
- 处理菜单展开和点击
- 验证导航成功
**导出函数:**
```python
class NavigationHandler:
def __init__(self, browser: BrowserOperator)
def navigate_to_module(self, module_name: str, module_name_son: str) -> bool
def find_menu_item(self, menu_text: str) -> str
def expand_menu(self, menu_uid: str) -> bool
def click_submenu(self, submenu_text: str) -> bool
def get_current_route(self) -> str
```
**导航流程:**
1. 获取页面快照
2. 查找一级菜单(module_name)
3. 如果菜单未展开,点击展开
4. 查找二级菜单(module_name_son)
5. 点击二级菜单
6. 等待页面加载
7. 从URL解析并返回路由
**依赖:**
- browser_operator模块
---
### 3.5 element_locator.py - 元素定位模块
**职责:**
- 智能检测元素定位方式
- 按优先级选择最优定位策略
- 生成多种定位方式作为备选
- 记录元素属性信息
**导出函数:**
```python
class ElementLocator:
def __init__(self, browser: BrowserOperator)
def locate_element(self, element_info: Dict) -> Dict
def get_locator_strategy(self, element: Dict) -> str
def generate_locator_value(self, element: Dict, strategy: str) -> str
def analyze_element_attributes(self, uid: str) -> Dict
```
**定位策略优先级:**
1. **ID属性**:如果元素有唯一id,优先使用
2. **Name属性**:如果元素有唯一name
3. **Class属性**:如果元素有唯一class
4. **XPATH表达式**:作为备选方案
- **注意****不允许使用UID**作为定位类型
- **注意**:XPATH中使用 `contains(.,'text')` 而非 `contains(text(),'text')`,以正确匹配Element UI等框架中span子元素内的文本
**输出格式:**
```python
{
"locator_type": "ID/XPATH/CSS_SELECTOR",
"locator_value": "//input[@id='username']",
"backup_locators": [...] # 备选定位方式
}
```
**依赖:**
- browser_operator模块
---
### 3.6 operation_executor.py - 操作执行模块
**职责:**
- 执行添加操作
- 执行编辑操作(创建→编辑→清理)
- 执行删除操作(创建→删除→清理)
- 记录操作步骤和元素信息
- 填写固定测试值
**导出函数:**
```python
class OperationExecutor:
def __init__(self, browser: BrowserOperator, locator: ElementLocator)
def execute_add(self) -> List[Dict]
def execute_edit(self) -> List[Dict]
def execute_delete(self) -> List[Dict]
def fill_test_data(self, form_elements: List[Dict]) -> None
def click_operation_button(self, operation: str) -> bool
def create_test_data(self) -> bool
def cleanup_test_data(self) -> bool
```
**固定测试值:**
```python
TEST_DATA = {
"username": "test001",
"password": "Test@123456",
"phone": "13800138000",
"email": "test001@example.com",
"name": "测试用户",
"description": "测试内容"
}
```
**操作流程:**
**添加操作:**
1. 点击"添加"按钮
2. 等待表单弹窗
3. 收集表单字段
4. 填写固定测试值
5. 点击"确定"按钮
6. **获取操作提示**(getTips - 弹窗提示)
7. **验证列表数据**(getText - 列表数据验证)
8. 记录所有步骤
**编辑操作:**
1. 执行添加操作创建测试数据
2. 点击"编辑"按钮
3. 修改部分字段
4. 点击"确定"按钮
5. **获取操作提示**(getTips - 弹窗提示)
6. **验证列表数据**(getText - 列表数据验证)
7. 删除测试数据
**删除操作:**
1. 执行添加操作创建测试数据
2. 点击"删除"按钮
3. 确认删除
4. **获取操作提示**(getTips - 弹窗提示)
5. **验证列表数据**(getText - 列表数据验证)
**依赖:**
- browser_operator模块
- element_locator模块
---
### 3.7 testcase_generator.py - 用例生成模块
**职责:**
- 根据操作步骤生成JSON测试用例
- 生成测试用例名称
- 填充page、step、expected_result字段
- 输出JSON文件到指定目录
**导出函数:**
```python
class TestCaseGenerator:
def __init__(self, module_info: Dict, base_url: str)
def generate_testcase(self, operation: str, steps: List[Dict]) -> Dict
def generate_name(self, operation: str) -> str
def generate_expected_result(self, operation: str) -> str
def save_testcase(self, testcase: Dict, output_dir: str) -> str
def format_json(self, data: Dict) -> str
```
**预期结果生成规则:**
```python
EXPECTED_RESULTS = {
"添加": "添加成功",
"编辑": "编辑成功",
"删除": "删除成功",
"提交": "提交成功",
"保存": "保存成功"
}
```
**依赖:**
- Python标准库:json、pathlib
---
### 3.8 main.py - 主控模块
**职责:**
- 统一调度各模块
- 处理用户输入(前台/后台选择)
- 控制整体执行流程
- 错误处理和日志记录
**导出函数:**
```python
class TestCaseGeneratorMain:
def __init__(self, system_config: str, module_config: str)
def run(self) -> None
def select_login_url(self, system: Dict) -> str
def process_modules(self) -> None
def process_single_module(self, module: Dict) -> None
def log(self, message: str, level: str = "INFO") -> None
```
**主流程:**
```python
def main():
# 1. 加载配置
system_config = load_system_config("system_config.json")
module_config = load_module_config("module_config.json")
# 2. 选择登录URL
login_url = select_login_url(system_config[0])
# 3. 初始化浏览器
browser = BrowserOperator()
browser.open_page(login_url)
# 4. 登录系统
login_handler = LoginHandler(browser)
login_handler.login(...)
# 5. 处理每个模块
for module in module_config:
process_single_module(module)
# 6. 输出报告
print("测试用例生成完成!")
```
**依赖:**
- 所有其他模块
---
## 4. 目录结构设计
```
ubains-module-test/
├── AuxiliaryTool/
│ └── TestCaseGenerator/ # 测试用例生成工具目录
│ ├── main.py # 主程序入口
│ ├── config_manager.py # 配置管理模块
│ ├── browser_operator.py # 浏览器操作模块
│ ├── login_handler.py # 登录模块
│ ├── navigation_handler.py # 导航模块
│ ├── element_locator.py # 元素定位模块
│ ├── operation_executor.py # 操作执行模块
│ ├── testcase_generator.py # 用例生成模块
│ ├── config/ # 配置文件目录
│ │ ├── system_config.json
│ │ └── module_config.json
│ ├── testcases/ # 输出测试用例目录
│ │ ├── 区域管理_增值服务_添加.json
│ │ ├── 区域管理_增值服务_编辑.json
│ │ └── 区域管理_增值服务_删除.json
│ └── utils/ # 工具函数
│ ├── __init__.py
│ ├── logger.py # 日志工具
│ └── constants.py # 常量定义
```
---
## 5. 支持的元素类型
### 5.1 基础类型
| element_type | 说明 | 操作方式 | 示例 |
|-------------|------|---------|------|
| click | 点击按钮、链接等 | 点击元素 | 点击【确定】按钮 |
| input | 文本输入框 | 输入文本 | 输入用户名 |
| select | 下拉选择框 | 选择选项 | 选择状态【启用】 |
### 5.2 选择类型
| element_type | 说明 | 操作方式 | 示例 |
|-------------|------|---------|------|
| checkbox | 复选框/单选框 | 勾选/取消勾选 | 勾选【启用】复选框 |
| switch | 开关控件 | 打开/关闭 | 打开【自动同步】开关 |
### 5.3 验证类型
| element_type | 说明 | 操作方式 | element_value | expected_result | 示例 |
|-------------|------|---------|-------------|----------------|------|
| getTips | 获取弹窗提示信息 | 读取Element UI等组件的提示文本 | 留空 `""` | 填写预期提示文本 | `"添加成功"` |
| getText | 获取列表数据文本 | 读取表格td等元素中的文本 | 留空 `""` | 填写验证描述 | `"列表中包含新添加的数据"` |
---
## 5.5 element_value 和 expected_result 填写规范
### 5.5.1 element_value 填写规则
| element_type | element_value 填写内容 | 示例 |
|-------------|----------------------|------|
| `input` | 填写需输入的文本内容 | `"test001"` |
| `getTips` | **留空** `""` | `""` |
| `getText` | **留空** `""` | `""` |
| `click` | **留空** `""` | `""` |
| `select` | **留空** `""` | `""` |
| `checkbox` | **留空** `""` | `""` |
| `switch` | **留空** `""` | `""` |
### 5.5.2 expected_result 填写规则
| element_type | expected_result 填写内容 | 示例 |
|-------------|-------------------------|------|
| `input` | 通常留空(非验证步骤) | `""` |
| `click` | 通常留空(非验证步骤) | `""` |
| `select` | 通常留空(非验证步骤) | `""` |
| `getTips` | 填写预期的弹窗提示文本 | `"添加成功"` |
| `getText` | 填写预期的验证描述 | `"列表中包含新添加的商品"` |
### 5.5.3 完整示例对比
**错误的写法**(element_value填写了验证相关内容):
```json
{
"element_type": "getTips",
"element_value": "添加成功", // 错误
"expected_result": ""
}
```
**正确的写法**(验证内容填写在expected_result):
```json
{
"element_type": "getTips",
"element_value": "", // 正确:留空
"expected_result": "添加成功" // 正确:填写预期结果
}
```
### 5.5.4 XPath 定位规范
**错误的写法**(无法匹配Element UI按钮span子元素内的文本):
```json
{
"locator_value": "//button[contains(text(),'确定')]" // 错误
}
```
**正确的写法**(使用 `contains(.,'text')` 匹配整个元素文本内容):
```json
{
"locator_value": "//button[contains(.,'确定')]" // 正确
}
```
**多条件定位的简化**(移除不必要的多条件组合):
```json
// 错误:多条件组合,定位器过长
"locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
// 正确:简化为唯一条件
"locator_value": "//button[contains(.,'确定')]"
```
---
## 6. 执行计划
### 6.1 阶段划分
| 阶段 | 任务 | 预计工作量 | 状态 |
|-----|------|----------|------|
| **阶段1** | 创建项目目录结构 | 0.5天 | ⏳ 待执行 |
| **阶段2** | 实现config_manager模块 | 0.5天 | ⏳ 待执行 |
| **阶段3** | 实现browser_operator模块 | 1-2天 | ⏳ 待执行 |
| **阶段4** | 实现login_handler模块 | 1天 | ⏳ 待执行 |
| **阶段5** | 实现navigation_handler模块 | 1天 | ⏳ 待执行 |
| **阶段6** | 实现element_locator模块 | 1-2天 | ⏳ 待执行 |
| **阶段7** | 实现operation_executor模块 | 2-3天 | ⏳ 待执行 |
| **阶段8** | 实现testcase_generator模块 | 1天 | ⏳ 待执行 |
| **阶段9** | 实现main主控模块 | 1天 | ⏳ 待执行 |
| **阶段10** | 集成测试 | 2-3天 | ⏳ 待执行 |
| **阶段11** | 文档编写 | 1天 | ⏳ 待执行 |
### 6.2 里程碑
| 里程碑 | 完成标准 | 状态 |
|-------|---------|------|
| M1: 基础模块完成 | config_manager、browser_operator、login_handler 完成 | ⏳ 待达成 |
| M2: 核心功能完成 | navigation、element_locator、operation_executor 完成 | ⏳ 待达成 |
| M3: 生成功能完成 | testcase_generator、main 完成 | ⏳ 待达成 |
| M4: 测试完成 | 端到端测试通过 | ⏳ 待达成 |
---
## 7. 测试计划
### 7.1 单元测试
每个模块需要测试:
- 函数正常执行
- 参数校验
- 异常处理
- 返回值格式
### 7.2 集成测试
测试场景:
1. 新平台系统完整流程
2. 多模块批量生成
3. 不同元素类型处理
4. 添加/编辑/删除操作
5. JSON输出格式验证
### 7.3 测试用例示例
**输入配置:**
```json
{
"module_name": "用户管理",
"module_name_son": "用户列表",
"module_function": ["添加"]
}
```
**预期输出:**
```json
{
"name": "用户管理_用户列表_添加",
"para": [
{
"page": "system/user",
"step": "点击左侧【用户管理】",
"locator_type": "XPATH",
"locator_value": "//span[contains(.,'用户管理')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "点击【用户列表】",
"locator_type": "XPATH",
"locator_value": "//li[contains(.,'用户列表')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "点击【添加】按钮",
"locator_type": "ID",
"locator_value": "addButton",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "输入用户名",
"locator_type": "ID",
"locator_value": "username",
"element_type": "input",
"element_value": "test001",
"expected_result": ""
},
{
"page": "system/user",
"step": "点击【确定】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(.,'确定')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "获取操作提示",
"locator_type": "XPATH",
"locator_value": "//p[@class='el-message__content']",
"element_type": "getTips",
"element_value": "",
"expected_result": "添加成功"
},
{
"page": "system/user",
"step": "验证列表数据",
"locator_type": "XPATH",
"locator_value": "//td[contains(.,'test001')]",
"element_type": "getText",
"element_value": "",
"expected_result": "列表中包含新添加的用户"
}
],
"platform": "web",
"base_url": "https://192.168.5.44/"
}
```
---
## 8. 风险与应对
| 风险 | 影响 | 概率 | 应对措施 |
|-----|------|------|---------|
| MCP连接不稳定 | 高 | 中 | 添加重试机制和超时处理 |
| 元素定位失败 | 高 | 中 | 提供多种定位方式备选 |
| 页面加载超时 | 中 | 中 | 添加等待和重试逻辑 |
| 登录验证码变化 | 中 | 低 | 支持手动输入或API获取 |
| 菜单结构差异 | 中 | 中 | 支持配置化导航路径 |
| 表单字段识别错误 | 中 | 中 | 添加字段类型识别 |
---
## 9. 验收标准
### 9.1 功能验收
- [ ] 能够正确读取配置文件
- [ ] 能够自动登录系统
- [ ] 能够正确导航到指定模块
- [ ] 能够智能检测元素定位方式
- [ ] 能够执行添加/编辑/删除操作
- [ ] 能够生成符合格式的JSON测试用例
- [ ] 能够批量处理多个模块
### 9.2 代码质量验收
- [ ] 符合代码规范要求(中文注释)
- [ ] 每个模块职责单一
- [ ] 函数有完整的类型注解
- [ ] 异常处理完善
- [ ] 日志记录详细
### 9.3 输出质量验收
- [ ] JSON格式正确
- [ ] 元素定位准确
- [ ] 不使用UID作为定位类型
- [ ] XPATH使用 `contains(.,'text')` 而非 `contains(text(),'text')`
- [ ] 定位器简洁,无多余的多条件组合
- [ ] 操作步骤完整
- [ ] 添加/编辑/删除操作包含getTips验证步骤
- [ ] 添加/编辑/删除操作包含getText列表验证步骤
- [ ] element_value填写正确
- [ ] 只有input类型填写输入内容
- [ ] getTips/getText类型element_value为空
- [ ] click/select类型element_value为空
- [ ] expected_result填写正确
- [ ] getTips类型填写预期提示文本
- [ ] getText类型填写验证描述
- [ ] 文件命名规范
---
## 10. 参考文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
- 需求文档: `Docs/PRD/自动化测试用例生成/_PRD_自动化测试用例生成需求文档.md`
---
## 11. 执行结果记录
### 11.1 代码实现完成(2026-03-06)
**完成内容:**
- ✅ 阶段1:创建项目目录结构
- ✅ 阶段2:实现config_manager模块
- ✅ 阶段3:实现browser_operator模块
- ✅ 阶段4:实现login_handler模块
- ✅ 阶段5:实现navigation_handler模块
- ✅ 阶段6:实现element_locator模块
- ✅ 阶段7:实现operation_executor模块
- ✅ 阶段8:实现testcase_generator模块
- ✅ 阶段9:实现main主控模块
**创建的文件:**
```
AuxiliaryTool/TestCaseGenerator/
├── main.py # 主程序入口(253行)
├── config_manager.py # 配置管理模块(227行)
├── browser_operator.py # 浏览器操作模块(277行)
├── login_handler.py # 登录处理模块(227行)
├── navigation_handler.py # 导航处理模块(221行)
├── element_locator.py # 元素定位模块(326行)
├── operation_executor.py # 操作执行模块(567行)
├── testcase_generator.py # 测试用例生成模块(249行)
├── config/
│ ├── system_config.json # 系统配置示例
│ └── module_config.json # 模块配置示例
├── utils/
│ ├── __init__.py
│ ├── logger.py # 日志工具
│ └── constants.py # 常量定义
├── testcases/ # 输出目录
└── README.md # 使用说明
```
**代码统计:**
- 总代码行数:约2300行
- 模块数量:8个核心模块
- 注释覆盖率:100%(所有函数包含中文注释)
**里程碑达成:**
- ✅ M1: 基础模块完成(config_manager、browser_operator、login_handler)
- ✅ M2: 核心功能完成(navigation、element_locator、operation_executor)
- ✅ M3: 生成功能完成(testcase_generator、main)
**状态:** ⏳ 待集成测试
### 11.2 问题修复记录(2026-03-06)
#### 问题1:XPath定位器文本匹配层级问题
**问题描述**
- `contains(text(), '文本')` 无法匹配Element UI按钮span子元素内的文本
- 定位器存在多条件组合,不够简洁
**修复方案**
| 修复项 | 修复前 | 修复后 |
|--------|--------|--------|
| 文本匹配 | `contains(text(), '确定')` | `contains(., '确定')` |
| 按钮定位 | `//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]` | `//button[contains(., '添加')]` |
| 确定按钮 | `//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定')]` | `//button[contains(., '确定')]` |
| 图标按钮 | `//span[contains(@id, 'edit') and .//i[contains(@class, 'el-icon-edit')]]` | `//i[contains(@class, 'el-icon-edit')]` |
**修复文件**
- `element_locator.py` - XPath生成逻辑
- `区域管理_增值服务_添加.json` - 步骤3、8
- `区域管理_增值服务_编辑.json` - 步骤3、5
- `区域管理_增值服务_删除.json` - 步骤3、4
#### 问题2:element_value 填写规则不统一
**问题描述**
- `getTips``getText` 类型的 `element_value` 填写了验证内容
- 规范要求验证内容应填写在 `expected_result` 字段中
**修复方案**
| element_type | element_value | expected_result |
|-------------|---------------|-----------------|
| `input` | 填写输入内容 | 通常留空 |
| `getTips` | **留空** ✅ | 填写预期提示文本 |
| `getText` | **留空** ✅ | 填写验证描述 |
| `click`/`select` | **留空** ✅ | 通常留空 |
**修复文件**
- `operation_executor.py` - `_get_operation_tips()``_verify_list_data()` 方法
- `区域管理_增值服务_添加.json` - 添加getTips步骤,修正element_value
- `区域管理_增值服务_编辑.json` - 添加getTips步骤,修正element_value
- `区域管理_增值服务_删除.json` - 添加getTips步骤,修正element_value
#### 问题3:新增getText类型区分验证场景
**说明**
- `getTips`:获取弹窗提示信息(如Element UI的el-message组件)
- `getText`:获取列表中的文本数据(如表格td元素)
**代码更新**
- `operation_executor.py` - 新增 `_verify_list_data()` 方法
- 各操作流程中添加列表数据验证步骤
#### 问题4:文档规范同步更新
**更新内容**
1. 需求文档新增3.4节:element_value 和 expected_result 填写规范
2. 需求文档更新示例:XPATH使用 `contains(.,'text')`
3. 计划执行文档新增5.5节:详细填写规范
4. 更新测试用例示例符合新规范
---
## 12. 优化功能回填
*本节将在执行过程中记录优化内容*
---
*文档结束*
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
},
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
\ No newline at end of file
# 自动化测试用例生成 - 执行问题修复文档
## 文档信息
- **创建日期**: 2026-03-06
- **版本**: v1.1
- **最后更新**: 2026-03-06 (第二轮验证)
- **状态**: 持续修复中
---
## 问题概述
通过实际执行生成的测试用例,发现了定位器相关的关键问题,导致测试用例无法正常执行。
---
## 发现的问题清单
### 问题1: ID重复冲突 ⚠️ 严重
**问题描述**
- 不同的HTML元素共享相同的ID属性
- 导致通过ID无法唯一识别目标元素
**具体案例**
```javascript
// 区域管理 > 增值服务页面
"分类管理"按钮: id="set_room_list.value_add-create" class="el-button el-button--success"
"添加"按钮: id="set_room_list.value_add-create" class="el-button el-button--primary"
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤3: 点击【添加】按钮
- 所有使用该ID定位的测试步骤
**问题表现**
```json
{
"step": "点击【添加】按钮",
"locator_type": "ID",
"locator_value": "set_room_list.value_add-create",
"result": "失败 - 打开了'分类管理'对话框而非'添加商品'对话框"
}
```
**修复方案**
```diff
- {
- "locator_type": "ID",
- "locator_value": "set_room_list.value_add-create"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]"
```
**修复状态**: ✅ 已完成
---
### 问题2: XPath语法错误 ⚠️ 严重
**问题描述**
- 生成的XPath表达式使用了从0开始的索引
- 标准XPath索引从1开始
**具体案例**
```json
// 错误的XPath格式
"locator_value": "body/div[0]/div[1]/div[2]/div[1]/div[2]/input[1]"
// ^^^ 索引从0开始,这是错误的
```
**正确的XPath语法**
```xpath
// XPath索引从1开始,不是从0开始
body/div[1]/div[1]/div[2]/div[1]/div[2]/input[1] // 正确
body/div[0]/div[1]/div[2]/div[1]/div[2]/input[1] // 错误
// 推荐使用更简洁的定位方式
//input[@placeholder='请输入商品名称'] // 最佳
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤4-7: 表单输入操作
- `区域管理_增值服务_编辑.json` - 步骤4: 商品名称输入
- 所有使用`div[0]`格式的XPath定位
**修复方案**
```diff
// 输入框定位 - 使用属性定位
- {
- "locator_type": "XPATH",
- "locator_value": "body/div[0]/div[1]/div[2]/div[1]/div[2]/input[1]"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//input[@placeholder='请输入商品名称']"
// 按钮定位 - 使用文本和类名组合
- {
- "locator_type": "XPATH",
- "locator_value": "//*[@id=\"app\"]/div[1]/div[1]/div[1]/div[1]/div[1]/div[7]/div[1]/div[2]/span[1]/button[2]"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
```
**修复状态**: ✅ 已完成
---
### 问题3: 提示信息定位不精确 ⚠️ 中等
**问题描述**
- 提示信息的XPath定位过于宽泛,可能匹配到错误的消息
**具体案例**
```json
// 原始定位 - 可能匹配到任意消息
"locator_value": "//*[@id=\"app\"]//div[contains(@class, \"el-message\")][last()]"
```
**改进方案**
```diff
- {
- "locator_type": "XPATH",
- "locator_value": "//*[@id=\"app\"]//div[contains(@class, \"el-message\")][last()]"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//div[contains(@class, 'el-message') and contains(text(), '添加成功')]"
```
**修复状态**: ✅ 已完成
---
### 问题4: el-message元素验证失败 ⚠️ 中等
**问题描述**
- Element UI的消息提示(el-message)元素存在时间非常短(通常1-2秒)
- 当测试执行到验证步骤时,消息元素已经消失
- 更新的定位器 `//p[@class='el-message__content']` 也无法捕获到元素
**具体案例**
```json
// 所有三个测试用例的最后一步都失败
步骤9: 验证"添加成功"消息 - 消息已消失
步骤6: 验证"修改成功"消息 - 消息已消失
步骤5: 验证"删除成功"消息 - 消息已消失
```
**实际状态**
- 所有操作本身都成功执行
- 列表数据正确更新(计数变化、内容变化)
- 成功消息确实显示过,但在验证时已消失
**解决方案建议**
1. **使用显式等待**:在验证步骤前添加等待逻辑,等待消息出现
2. **使用快照验证**:在操作后立即保存页面状态快照
3. **使用数据验证**:验证列表数据变化而非提示消息
4. **延长消息显示时间**:修改Element UI配置(需要开发支持)
**修复状态**: ⚠️ 需要进一步优化
---
### 问题5: XPath文本匹配层级问题 ⚠️ 严重
**问题描述**
- `text()`函数只匹配元素的直接文本节点,不包含子元素的文本
- Element UI按钮的文本通常在`<span>`子元素内,不在button的直接文本中
**具体案例**
```javascript
// 按钮的实际结构
<button class="el-button el-button--primary" id="...">
<span>添加</span> <!-- 文本在span内,不是button的直接文本 -->
</button>
// 错误的XPath(无法匹配)
//button[contains(text(), '添加')]
// 正确的XPath
//button[.//text()='添加'] 或 //button[contains(., '添加')]
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤3: 点击【添加】按钮
- 所有使用`contains(text(), '文本')`格式匹配按钮的场景
**正确修复方案**
```diff
- "locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and .//text()='添加']"
或者更简洁的写法:
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '添加')]"
```
**修复状态**: ⚠️ 待修复
---
### 问题6: 确定按钮XPath定位失败 ⚠️ 严重
**问题描述**
- 确定按钮的XPath使用了与问题5相同的`text()`匹配方式
- 同时`el-dialog__footer`类名可能不存在
**具体案例**
```json
// JSON中的定位器
"locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
// 实际DOM中可能使用的是
<div class="dialog-footer">...</div> <!-- 注意没有el-前缀 -->
```
**修复方案**
需要同时修复两个问题:
1. 类名匹配
2. 文本匹配方式
**修复状态**: ⚠️ 待修复
---
## 修复后的定位器策略
### 推荐的定位器优先级
| 优先级 | 定位器类型 | 适用场景 | 示例 |
|--------|-----------|---------|------|
| 1 | ID | 唯一ID且不重复 | `id="unique-element"` |
| 2 | 属性定位 | 有唯一属性值 | `//input[@placeholder='唯一文本']` |
| 3 | 文本+属性组合 | 需要结合多个特征 | `//button[contains(text(), '确定') and contains(@class, 'primary')]"` |
| 4 | 文本定位 | 元素有唯一文本 | `//button[text()='确定']` |
| 5 | CSS选择器 | 复杂定位场景 | `button.el-button--primary` |
### XPath生成规范
**必须遵循的规则**
1. 索引从1开始:`div[1]` ✅,`div[0]`
2. 使用属性定位优于层级定位:`//input[@id='x']` ✅,`//div[1]/div[2]/input[1]` ⚠️
3. 使用相对路径优于绝对路径:`//input[@placeholder='x']` ✅,`/body/div[1]/.../input[1]`
4. 使用contains()匹配部分文本:`//button[contains(text(), '确定')]`
---
## 修复后的测试用例
### 1. 区域管理_增值服务_添加.json
```json
{
"name": "区域管理_增值服务_添加",
"para": [
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "点击【添加】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "输入商品名称",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='请输入商品名称']",
"element_type": "input",
"element_value": "测试商品001"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "点击商品分类选择框",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='请选择商品分类']",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "选择【商品】分类",
"locator_type": "XPATH",
"locator_value": "//li[contains(text(), '商品')]",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "输入商品描述",
"locator_type": "XPATH",
"locator_value": "//textarea[@placeholder='请输入商品描述']",
"element_type": "input",
"element_value": "自动化测试商品描述"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "点击【确定】按钮",
"locator_type": "XPATH",
"locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "获取提示文本",
"locator_type": "XPATH",
"locator_value": "//div[contains(@class, 'el-message') and contains(text(), '添加成功')]",
"element_type": "getTips",
"element_value": "添加成功",
"expected_result": "添加成功"
}
]
}
```
### 2. 区域管理_增值服务_编辑.json
**关键修复点**:
- 编辑按钮定位:使用XPath匹配包含`edit`的ID和图标类名
- 确定按钮定位:与添加操作相同
### 3. 区域管理_增值服务_删除.json
**关键修复点**:
- 删除按钮定位:使用XPath匹配包含`delete`的ID和图标类名
- 确认对话框定位:使用`el-message-box`类名定位
---
## 代码修复建议
### 测试用例生成器需要添加的验证逻辑
```python
def validate_locator(locator_type: str, locator_value: str) -> tuple[bool, str]:
"""
验证定位器是否有效
Returns:
(is_valid, error_message)
"""
# 检查1: XPath索引是否从0开始(错误)
if locator_type == "XPATH" and "[0]" in locator_value:
return False, "XPath索引不能从0开始,应该从1开始"
# 检查2: ID定位器是否在当前页面唯一
if locator_type == "ID":
elements = driver.find_elements(By.ID, locator_value)
if len(elements) > 1:
return False, f"ID '{locator_value}' 不唯一,找到{len(elements)}个元素"
if len(elements) == 0:
return False, f"ID '{locator_value}' 未找到任何元素"
# 检查3: 验证XPath是否有效
if locator_type == "XPATH":
try:
elements = driver.find_elements(By.XPATH, locator_value)
if len(elements) == 0:
return False, f"XPath未找到任何元素: {locator_value}"
if len(elements) > 1:
return False, f"XPath找到{len(elements)}个元素,不唯一: {locator_value}"
except Exception as e:
return False, f"XPath语法错误: {str(e)}"
return True, ""
```
---
## 测试验证结果
### 第一轮验证结果
| 测试用例 | 修复前状态 | 修复后状态 | 备注 |
|---------|-----------|-----------|------|
| 区域管理_增值服务_添加 | ❌ 步骤3、4、8失败 | ✅ 操作成功 | 定位器修复有效 |
| 区域管理_增值服务_编辑 | ❌ 步骤3、4失败 | ✅ 操作成功 | 定位器修复有效 |
| 区域管理_增值服务_删除 | ⚠️ 步骤3可能失败 | ✅ 操作成功 | 定位器修复有效 |
### 第二轮验证结果(2026-03-06)
| 测试用例 | 操作执行 | 消息验证 | 遇到的问题 |
|---------|---------|---------|-----------|
| 区域管理_增值服务_添加 | ✅ 成功 | ❌ 定位器失败 | 问题5、6 |
| 区域管理_增值服务_编辑 | - | - | 待验证 |
| 区域管理_增值服务_删除 | - | - | 待验证 |
**详细问题**
- 步骤3(添加按钮):`contains(text(), '添加')` 无法匹配span内的文本
- 步骤8(确定按钮):类名`el-dialog__footer`可能不准确
- 步骤9(消息验证):`//p[@class='el-message__content']` 定位器失败
---
## 后续优化方向
### 1. 定位器智能选择
- 实现定位器优先级算法
- 当ID不唯一时,自动选择替代定位方式
### 2. XPath生成优化
- 修复索引从0开始的问题
- 优先生成属性定位而非层级定位
- 简化XPath表达式
### 3. 定位器验证机制
- 生成测试用例前进行定位器验证
- 对每个定位器进行唯一性检查
- 输出验证报告
### 4. 错误处理增强
- 当定位器不唯一时,提供多个备选方案
- 在测试用例中标注定位器稳定性评分
---
## 总结
### 第一轮验证(2026-03-06)
本次执行验证发现了测试用例生成过程中的三个关键问题:
1. **ID重复冲突** - 需要改用XPath或CSS选择器
2. **XPath语法错误** - 索引从0开始导致定位失败
3. **提示信息定位不精确** - 可能匹配到错误的消息
所有问题已通过重写测试用例文件得到修复。
### 第二轮验证(2026-03-06)
在用户更新了消息定位器后重新验证,发现了新的关键问题:
4. **el-message元素验证失败** - 消息存在时间太短(1-2秒),验证时已消失
5. **XPath文本匹配层级问题** - `text()`只匹配直接文本,不包含子元素文本
6. **确定按钮XPath定位失败** - 类名和文本匹配都存在问题
**当前状态**
- ✅ 操作执行成功(添加、编辑、删除都能完成)
- ✅ 列表数据验证成功
- ⚠️ 按钮定位器需要修复(问题5、6)
- ⚠️ 消息验证需要改进方案(问题4)
**建议优先修复**
1. 修复所有使用`contains(text(), 'xxx')`的定位器,改为`contains(., 'xxx')``.//text()='xxx'`
2. 验证`el-dialog__footer`类名的准确性
3. 为消息验证添加显式等待或使用数据验证替代
---
**文档结束**
# 自动化测试用例生成 - 问题描述文档
## 文档信息
- **创建日期**: 2026-03-06
- **验证轮次**: 第二轮
- **验证范围**: 添加操作测试用例
- **状态**: 待修复
---
## 问题概述
在用户更新了消息定位器后,重新执行添加操作测试用例时,发现了新的关键问题:
1. XPath文本匹配层级问题导致按钮定位失败
2. 确定按钮定位器类名和文本匹配都存在问题
3. 消息元素验证仍然失败(存在时间太短)
---
## 问题清单
### 问题1: 添加按钮XPath定位失败 ⚠️ 严重
**问题描述**
测试用例中的XPath `//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]` 无法定位到添加按钮。
**根因分析**
XPath的`text()`函数只匹配元素的直接文本节点,不包含子元素的文本。Element UI按钮的实际HTML结构为:
```html
<button type="button" class="el-button el-button--primary" id="set_room_list.value_add-create">
<span>添加</span>
</button>
```
"添加"文本在`<span>`子元素内,不是`<button>`的直接文本,因此`contains(text(), '添加')`无法匹配。
**错误定位器**
```xpath
//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]
```
**正确做法**
```xpath
// 使用点号(.)匹配所有后代文本
//button[contains(@class, 'el-button--primary') and contains(., '添加')]
// 或使用 descendant-or-self axis
//button[contains(@class, 'el-button--primary') and .//text()='添加']
// 或直接定位span子元素
//button[contains(@class, 'el-button--primary')]//span[text()='添加']
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤3
- 所有使用`contains(text(), 'xxx')`格式匹配Element UI按钮的场景
**测试结果**
```json
{
"step": 3,
"success": false,
"xpath": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]",
"message": "XPath定位失败 - 未找到添加按钮"
}
```
---
### 问题2: 确定按钮XPath定位失败 ⚠️ 严重
**问题描述**
测试用例中的XPath `//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]` 无法定位到确定按钮。
**根因分析**
存在两个问题:
1. **类名问题**`el-dialog__footer` 类名在Element UI中可能不使用`el-`前缀,实际可能是`dialog-footer`
2. **文本匹配问题**:与问题1相同,`contains(text(), '确定')`无法匹配span内的文本
**错误定位器**
```xpath
//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]
```
**修复建议**
```xpath
// 简化定位器,直接查找确定按钮
//button[contains(@class, 'el-button--primary') and contains(., '确定')]
// 或使用更通用的定位方式
//button[contains(@class, 'el-button--primary')]//span[text()='确定']
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤8
- `区域管理_增值服务_编辑.json` - 步骤5
- 所有对话框确定按钮的定位
**测试结果**
```json
{
"step": 8,
"success": true,
"message": "使用备用定位器成功点击确定按钮",
"note": "原始XPath定位失败"
}
```
---
### 问题3: 消息元素验证失败 ⚠️ 中等
**问题描述**
更新后的定位器 `//p[@class='el-message__content']` 仍然无法捕获到消息元素。
**根因分析**
Element UI的`el-message`组件默认显示时间约为1-2秒,当测试执行到验证步骤时,消息元素已经从DOM中移除。
**当前定位器**
```xpath
//p[@class='el-message__content']
```
**问题表现**
- 操作成功执行(列表数据正确更新)
- 成功消息确实显示过
- 但验证时消息已消失
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤9
- `区域管理_增值服务_编辑.json` - 步骤6
- `区域管理_增值服务_删除.json` - 步骤5
- 所有消息验证步骤
**测试结果**
```json
{
"step": 9,
"success": false,
"xpath": "//p[@class='el-message__content']",
"message": "XPath定位失败 - 未找到提示信息"
}
```
---
## 测试执行详情
### 添加操作测试用例执行记录
| 步骤 | 操作 | 定位器 | 结果 | 备注 |
|------|-----------|--------------|--------|-------|
| 1-2 | 导航到页面 | ID | ✅ 跳过 | 已在页面 |
| 3 | 点击【添加】按钮 | XPATH | ❌ 失败 | 问题1 - 使用UID通过 |
| 4 | 输入商品名称 | XPATH | ✅ 成功 | placeholder定位正常 |
| 5 | 点击商品分类选择框 | XPATH | ✅ 成功 | placeholder定位正常 |
| 6 | 选择分类 | XPATH | ✅ 成功 | 文本定位正常 |
| 7 | 输入商品描述 | XPATH | ✅ 成功 | placeholder定位正常 |
| 8 | 点击【确定】按钮 | XPATH | ⚠️ 备用 | 问题2 - 使用UID通过 |
| 9 | 获取提示文本 | XPATH | ❌ 失败 | 问题3 - 消息已消失 |
**实际操作结果**
- ✅ "测试商品001"成功添加到列表
- ✅ 列表计数从4条变为5条
- ✅ 成功消息"添加成功"确实显示过
- ❌ 验证步骤失败
---
## 技术分析
### XPath文本匹配机制
在XPath中,`text()`的行为:
```javascript
// 只匹配直接文本节点
text() // 元素的直接子文本节点
// 匹配所有文本(包括后代)
. // 当前元素的所有文本内容
text() // 第一个文本节点
//text() // 所有后代文本节点
// 实际示例
<button><span>添加</span></button>
text() = "" // 空,无直接文本
. = "添加" // 包含所有后代文本
.//text() = "添加" // 后代文本
span/text() = "添加" // 子元素文本
```
### Element UI消息组件生命周期
```javascript
// Element UI 消息默认配置
{
duration: 3000, // 显示3秒(但实际可能更短)
showClose: false, // 不显示关闭按钮
center: false, // 不居中
onClose: null // 关闭回调
}
// 消息出现后会在指定时间后自动移除DOM
// 因此测试验证必须在短时间内完成
```
---
## 影响评估
### 严重性评估
| 问题 | 严重性 | 可执行性 | 数据准确性 |
|------|-------|---------|-----------|
| 问题1: 添加按钮定位失败 | 严重 | ❌ 无法执行 | - |
| 问题2: 确定按钮定位失败 | 严重 | ⚠️ 需变通 | ✅ |
| 问题3: 消息验证失败 | 中等 | ✅ 可执行 | ✅ |
### 影响范围统计
- **测试用例文件数量**: 3个(添加、编辑、删除)
- **受影响的步骤**: 约9个步骤
- **必须修复的问题**: 2个(问题1、问题2)
- **可选优化的问题**: 1个(问题3)
---
## 附录
### 相关文档
- 第一轮问题修复文档:`_PRD_执行问题修复.md`
- 测试用例文件:`AuxiliaryTool/TestCaseGenerator/testcases/`
### 验证环境
- 系统地址:https://192.168.5.44
- 测试时间:2026-03-06
- 浏览器:Chrome (via chrome-devtools MCP)
---
**文档结束**
# 自动化测试用例生成 - 问题修复计划执行文档
## 文档信息
- **创建日期**: 2026-03-06
- **版本**: v1.0
- **状态**: 待执行
---
## 修复目标
修复第二轮验证中发现的XPath文本匹配层级问题和消息验证问题,确保测试用例能够稳定执行。
---
## 修复计划清单
### 任务1: 修复按钮定位器的XPath表达式 ✅ 必须修复
**优先级**: P0 - 严重
**预计工时**: 1小时
**问题描述**
`contains(text(), '文本')` 无法匹配Element UI按钮span子元素内的文本
**修复步骤**
1. **修改添加按钮定位器**
```diff
文件: 区域管理_增值服务_添加.json
步骤: 3 - 点击【添加】按钮
- "locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '添加')]"
```
2. **修改确定按钮定位器 - 添加用例**
```diff
文件: 区域管理_增值服务_添加.json
步骤: 8 - 点击【确定】按钮
- "locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '确定')]"
```
3. **修改确定按钮定位器 - 编辑用例**
```diff
文件: 区域管理_增值服务_编辑.json
步骤: 5 - 点击【确定】按钮
- "locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '确定')]"
```
4. **检查其他按钮定位器**
- 搜索所有使用 `contains(text(),` 的XPath
- 确认是否需要修复
**验证方法**
```bash
# 重新执行添加操作测试用例
# 期望:步骤3、8的XPath定位成功
```
**验收标准**
- ✅ 添加按钮定位成功
- ✅ 确定按钮定位成功
- ✅ 无需使用UID等变通方式
---
### 任务2: 优化消息验证方案 ⚠️ 可选优化
**优先级**: P1 - 中等
**预计工时**: 2小时
**问题描述**
el-message元素存在时间太短(1-2秒),XPath验证时已消失
**可选方案**
#### 方案A: 使用数据验证替代消息验证(推荐)
将消息验证改为验证列表数据的变化:
```diff
文件: 区域管理_增值服务_添加.json
步骤: 9 - 验证添加结果
- {
- "step": "获取提示文本",
- "locator_type": "XPATH",
- "locator_value": "//p[@class='el-message__content']",
- "element_type": "getTips",
- "element_value": "添加成功",
- "expected_result": "添加成功"
- }
+ {
+ "step": "验证列表数据",
+ "locator_type": "XPATH",
+ "locator_value": "//td[contains(text(), '测试商品001')]",
+ "element_type": "verify",
+ "element_value": "存在",
+ "expected_result": "列表中包含新添加的商品"
+ }
```
#### 方案B: 在测试执行框架中添加显式等待
```python
def wait_for_message(driver, text, timeout=3):
"""等待消息出现并返回"""
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
try:
element = WebDriverWait(driver, timeout).until(
EC.presence_of_element_located((By.XPATH, f"//p[@class='el-message__content' and contains(text(), '{text}')]"))
)
return element.text
except:
return None
```
#### 方案C: 延长消息显示时间(需要开发支持)
修改Element UI全局配置:
```javascript
// main.js 或相关配置文件
Vue.prototype.$message = Message
Message.defaults.duration = 5000 // 延长到5秒
```
**推荐方案**: 方案A(数据验证)
- 更可靠
- 不依赖消息显示时间
- 直接验证业务结果
**验收标准**
- ✅ 验证步骤能够成功执行
- ✅ 验证结果准确反映操作是否成功
---
## 代码修复清单
### 需要修改的文件
| 文件路径 | 修改内容 | 影响步骤 |
|---------|---------|---------|
| `testcases/区域管理_增值服务_添加.json` | 修复步骤3、8的XPath | 2个步骤 |
| `testcases/区域管理_增值服务_编辑.json` | 修复步骤5的XPath | 1个步骤 |
| `testcases/区域管理_增值服务_删除.json` | 检查确定按钮XPath | 1个步骤 |
### 测试用例生成器修复(可选)
如果要从根本上避免此类问题,需要在测试用例生成器中添加:
```python
# 在生成XPath时,使用 . 而不是 text()
def generate_button_xpath(text, class_name=None):
"""
生成按钮的XPath定位器
Args:
text: 按钮文本
class_name: 按钮类名
"""
if class_name:
# 使用 contains(., 'text') 而不是 contains(text(), 'text')
return f"//button[contains(@class, '{class_name}') and contains(., '{text}')]"
return f"//button[contains(., '{text}')]"
```
---
## 修复执行计划
### Phase 1: 紧急修复(必须) - 预计1小时
1. [ ] 修复添加操作测试用例的XPath(步骤3、8)
2. [ ] 修复编辑操作测试用例的XPath(步骤5)
3. [ ] 检查删除操作测试用例的XPath
4. [ ] 重新执行添加操作测试用例验证
### Phase 2: 验证测试 - 预计30分钟
1. [ ] 完整执行添加操作测试用例
2. [ ] 完整执行编辑操作测试用例
3. [ ] 完整执行删除操作测试用例
4. [ ] 记录测试结果
### Phase 3: 优化改进(可选) - 预计1.5小时
1. [ ] 实施数据验证方案(替代消息验证)
2. [ ] 更新所有三个测试用例的验证步骤
3. [ ] 再次验证所有测试用例
4. [ ] 更新问题文档
---
## XPath修复规范
### 修复前 vs 修复后对比
| 场景 | 错误写法 | 正确写法 |
|------|---------|---------|
| 按钮文本匹配 | `contains(text(), '确定')` | `contains(., '确定')` |
| 按钮精确定位 | `//button[contains(text(), '确定')]` | `//button[contains(., '确定')]` |
| 组合定位 | `//button[contains(text(), '确定') and contains(@class, 'primary')]` | `//button[contains(@class, 'primary') and contains(., '确定')]` |
### 关键规则
1. **匹配包含的文本**:使用 `contains(., 'text')` 而不是 `contains(text(), 'text')`
2. **精确匹配文本**:使用 `.//text()='text'` 而不是 `text()='text'`
3. **简化定位器**:优先使用直接定位而非多层嵌套
---
## 验证计划
### 修复后验证步骤
1. **单步验证**
- 重新执行添加操作测试用例
- 逐步验证每个XPath定位器是否工作
- 记录每步的执行结果
2. **完整验证**
- 连续执行添加→编辑→删除操作
- 验证测试用例的连续性和稳定性
- 确认所有操作都能成功完成
3. **回归验证**
- 检查之前的修复是否仍然有效
- 确保没有引入新的问题
### 成功标准
- ✅ 所有XPath定位器能够正确找到元素
- ✅ 无需使用UID等变通方式
- ✅ 添加、编辑、删除操作都能成功执行
- ✅ 验证步骤能够准确反映操作结果
---
## 风险评估
### 潜在风险
1. **其他按钮定位器可能存在相同问题**
- 风险等级:中
- 缓解措施:全局搜索并检查所有按钮定位器
2. **编辑/删除操作可能遇到类似问题**
- 风险等级:高
- 缓解措施:先验证这两个测试用例
3. **数据验证方案可能不够全面**
- 风险等级:低
- 缓解措施:可以保留消息验证作为可选验证
---
## 附录
### 相关文档
- 问题描述文档:`_PRD_问题描述_问题记录_20260306_第二轮.md`
- 第一轮修复文档:`_PRD_执行问题修复.md`
### 修复历史
| 日期 | 轮次 | 修复内容 | 状态 |
|------|------|---------|------|
| 2026-03-06 | 第一轮 | ID重复、XPath索引、消息定位 | 已完成 |
| 2026-03-06 | 第二轮 | XPath文本匹配层级 | 待执行 |
---
**文档结束**
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论