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

docs(test-case-generator): 更新测试用例生成需求文档并移除旧代码文件

- 添加 system_type 字段用于区分前后台系统类型
- 更新 module_config.json 格式说明,增加系统类型判断逻辑
- 优化 Page 字段获取说明,强调从当前页面URL提取真实路由
- 明确测试用例不包含登录步骤,假设系统已处于登录状态
- 移除废弃的工具函数模块和浏览器操作模块代码文件
- 更新功能操作策略,明确清理操作步骤需记录到测试用例中
上级 588166f7
......@@ -119,7 +119,8 @@
"mcp__chrome-devtools__click",
"mcp__chrome-devtools__wait_for",
"mcp__chrome-devtools__press_key",
"mcp__chrome-devtools__new_page"
"mcp__chrome-devtools__new_page",
"mcp__chrome-devtools__close_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 -*-
"""
浏览器操作模块
使用Selenium WebDriver提供浏览器操作接口
"""
import time
import uuid
from typing import Dict, List, Optional, Any
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.remote.webelement import WebElement
from webdriver_manager.chrome import ChromeDriverManager
from utils.logger import get_logger
from utils.constants import WAIT_TIMEOUT, WAIT_SHORT, MCP_TIMEOUT
class BrowserOperator:
"""浏览器操作器 - 使用Selenium WebDriver"""
def __init__(self, headless: bool = False):
"""
初始化浏览器操作器
Args:
headless: 是否使用无头模式
"""
self.logger = get_logger("BrowserOperator")
self.driver: Optional[webdriver.Chrome] = None
self.current_url: Optional[str] = None
self.page_id: Optional[int] = None
self.is_connected = False
self._element_counter = 0
self._uid_to_element: Dict[str, WebElement] = {}
# 初始化浏览器
self._init_browser(headless)
def _init_browser(self, headless: bool) -> None:
"""初始化Chrome浏览器"""
try:
chrome_options = Options()
if headless:
chrome_options.add_argument("--headless")
# 常用Chrome选项
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("--lang=zh-CN")
# 忽略SSL错误
chrome_options.add_argument("--ignore-certificate-errors")
chrome_options.add_argument("--allow-running-insecure-content")
# 自动安装ChromeDriver
service = Service(ChromeDriverManager().install())
self.driver = webdriver.Chrome(service=service, options=chrome_options)
self.driver.implicitly_wait(10)
self.is_connected = True
self.logger.info("浏览器初始化成功")
except Exception as e:
self.logger.error(f"浏览器初始化失败: {e}")
raise
def open_page(self, url: str, timeout: int = MCP_TIMEOUT) -> bool:
"""
打开新页面并导航到指定URL
Args:
url: 目标URL
timeout: 超时时间(毫秒)
Returns:
是否成功打开页面
"""
self.logger.info(f"正在打开页面: {url}")
try:
self.driver.get(url)
self.current_url = self.driver.current_url
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:
if ignore_cache:
self.driver.delete_all_cookies()
self.driver.get(url)
self.current_url = self.driver.current_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:
"""
获取页面快照(模拟MCP格式)
Args:
verbose: 是否包含详细信息
Returns:
页面快照字典,包含元素uid和相关信息
"""
self.logger.debug("正在获取页面快照")
try:
elements = []
self._uid_to_element.clear()
# 获取所有输入框
try:
inputs = self.driver.find_elements(By.XPATH, "//input")
for idx, elem in enumerate(inputs):
uid = f"input-{idx}"
self._uid_to_element[uid] = elem
element_info = self._extract_element_info(elem, uid, "textbox")
elements.append(element_info)
except Exception as e:
self.logger.debug(f"获取输入框时出错: {e}")
# 获取所有按钮
try:
buttons = self.driver.find_elements(By.XPATH, "//button")
for idx, elem in enumerate(buttons):
uid = f"button-{idx}"
self._uid_to_element[uid] = elem
element_info = self._extract_element_info(elem, uid, "button")
elements.append(element_info)
except Exception as e:
self.logger.debug(f"获取按钮时出错: {e}")
# 获取所有文本区域
try:
textareas = self.driver.find_elements(By.XPATH, "//textarea")
for idx, elem in enumerate(textareas):
uid = f"textarea-{idx}"
self._uid_to_element[uid] = elem
element_info = self._extract_element_info(elem, uid, "textbox")
elements.append(element_info)
except Exception as e:
self.logger.debug(f"获取文本区域时出错: {e}")
snapshot = {
"elements": elements,
"url": self.driver.current_url if self.driver else "",
"title": self.driver.title if self.driver else ""
}
self.logger.debug(f"页面快照获取成功,共 {len(elements)} 个元素")
return snapshot
except Exception as e:
self.logger.error(f"获取页面快照失败: {e}")
return {"elements": [], "url": "", "title": ""}
def _extract_element_info(self, element: WebElement, uid: str, default_role: str) -> Dict:
"""
提取元素信息
Args:
element: Selenium WebElement
uid: 元素唯一标识
default_role: 默认角色
Returns:
元素信息字典
"""
info = {
"uid": uid,
"role": default_role,
"name": "",
"attributes": {}
}
try:
# 获取文本内容
try:
text = element.text
if text:
info["name"] = text
except:
pass
# 获取属性
attributes = ["id", "name", "type", "class", "placeholder", "value", "role", "aria-label"]
for attr in attributes:
try:
value = element.get_attribute(attr)
if value:
info["attributes"][attr] = value
# 如果name为空,使用placeholder作为name
if not info["name"] and attr == "placeholder":
info["name"] = value
except:
pass
# 获取标签名
try:
info["attributes"]["tag"] = element.tag_name
except:
pass
except Exception as e:
self.logger.debug(f"提取元素信息时出错: {e}")
return info
def find_element_by_text(self, snapshot: Dict, text: str, exact_match: bool = False) -> Optional[str]:
"""
在页面快照中查找包含指定文本的元素
搜索范围:
1. 元素的 name 字段(可访问性名称)
2. 元素的 placeholder 属性
Args:
snapshot: 页面快照
text: 要查找的文本
exact_match: 是否精确匹配
Returns:
元素的uid,如果未找到则返回None
"""
self.logger.debug(f"正在查找元素: {text}")
try:
elements = snapshot.get("elements", [])
for element in elements:
# 策略1: 搜索 name 字段
element_text = element.get("name", "")
if exact_match:
if element_text == text:
uid = element.get("uid")
self.logger.debug(f"找到元素(name匹配): {text}, uid: {uid}")
return uid
else:
if text in element_text:
uid = element.get("uid")
self.logger.debug(f"找到元素(name匹配): {text}, uid: {uid}")
return uid
# 策略2: 搜索 placeholder 属性
attrs = element.get("attributes", {})
placeholder = attrs.get("placeholder", "")
if placeholder:
if exact_match:
if placeholder == text:
uid = element.get("uid")
self.logger.debug(f"找到元素(placeholder匹配): {text}, uid: {uid}")
return uid
else:
if text in placeholder:
uid = element.get("uid")
self.logger.debug(f"找到元素(placeholder匹配): {text}, uid: {uid}")
return uid
self.logger.warning(f"未找到元素: {text}")
return None
except Exception as e:
self.logger.error(f"查找元素失败: {e}")
return None
def find_element_by_attributes(self, snapshot: Dict, **attributes) -> Optional[str]:
"""
通过属性查找元素
Args:
snapshot: 页面快照
**attributes: 要匹配的属性键值对,如 placeholder="用户名"
Returns:
元素的uid,如果未找到则返回None
"""
try:
elements = snapshot.get("elements", [])
for element in elements:
match = True
for key, value in attributes.items():
attrs = element.get("attributes", {})
element_value = str(attrs.get(key, ""))
if value not in element_value:
match = False
break
if match:
uid = element.get("uid")
self.logger.debug(f"找到匹配元素(属性: {attributes}), uid: {uid}")
return uid
self.logger.warning(f"未找到匹配元素的属性: {attributes}")
return None
except Exception as e:
self.logger.error(f"通过属性查找元素失败: {e}")
return None
def find_inputs_by_role(self, snapshot: Dict, role: str = "textbox") -> List[Dict]:
"""
查找所有指定角色的输入元素
Args:
snapshot: 页面快照
role: 角色,默认为textbox
Returns:
匹配的元素列表,包含uid和属性信息
"""
try:
elements = snapshot.get("elements", [])
matched_elements = []
for element in elements:
element_role = element.get("role", "")
if element_role == role:
matched_elements.append({
"uid": element.get("uid"),
"attributes": element.get("attributes", {})
})
self.logger.debug(f"找到 {len(matched_elements)} 个 role='{role}' 的元素")
return matched_elements
except Exception as e:
self.logger.error(f"查找输入元素失败: {e}")
return []
def click_element(self, uid: str, wait_after: int = WAIT_SHORT) -> bool:
"""
点击指定元素
Args:
uid: 元素的唯一标识符
wait_after: 点击后等待时间(秒)
Returns:
是否成功点击
"""
self.logger.debug(f"正在点击元素: {uid}")
try:
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return False
element.click()
# 等待页面响应
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:
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return False
if clear_first:
element.clear()
element.send_keys(value)
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:
from selenium.webdriver.support.select import Select
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return False
select = Select(element)
select.select_by_visible_text(value)
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
"""
try:
if self.driver:
return self.driver.current_url
except:
pass
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:
if self.driver:
result = self.driver.execute_script(script)
self.logger.debug("脚本执行成功")
return result
return None
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:
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return {}
# 获取所有属性
attrs = {}
for attr in ["id", "name", "class", "type", "placeholder", "value", "role", "aria-label"]:
try:
value = element.get_attribute(attr)
if value:
attrs[attr] = value
except:
pass
self.logger.debug(f"元素属性获取成功: {uid}")
return attrs
except Exception as e:
self.logger.error(f"获取元素属性失败: {e}")
return {}
def close(self) -> None:
"""关闭浏览器"""
self.logger.info("正在关闭浏览器")
try:
if self.driver:
self.driver.quit()
self.driver = None
self.is_connected = False
self.current_url = None
self._uid_to_element.clear()
self.logger.info("浏览器已关闭")
except Exception as e:
self.logger.error(f"关闭浏览器失败: {e}")
[
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加", "编辑", "删除"]
}
]
{
"system_type": "后台系统",
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
},
{
"system_type": "前台系统",
"module_name": "会议室管理",
"module_name_son": "会议室列表",
"module_function": ["添加","编辑"]
}
]
\ No newline at end of file
[
{
{
"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",
"back_username": ["XPATH","//input[@placeholder='请输入账号或手机号或邮箱号']","admin@xty"],
"back_password": ["XPATH","//input[@placeholder='请输入密码']","Ubains@4321"],
"back_code": ["XPATH","//input[@placeholder='请输入图形验证码']","csba"],
"front_username": ["XPATH","//input[@placeholder='手机号/用户名/邮箱']","admin@xty"],
"front_password": ["XPATH","//input[@placeholder='密码']","Ubains@4321"],
"front_code": ["XPATH","//input[@placeholder='图形验证']","csba"]
}
]
"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
# -*- 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"]
# 登录字段(格式:["定位类型", "定位值", "输入值"])
login_fields = ["back_username", "back_password", "back_code",
"front_username", "front_password", "front_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
# 检查登录字段格式(如果存在)
for field in login_fields:
if field in system:
field_value = system[field]
if not isinstance(field_value, list) or len(field_value) != 3:
self.logger.error(f"登录字段 {field} 格式错误,应为 [定位类型, 定位值, 输入值]")
return False
# 验证定位类型
locator_type = field_value[0]
if locator_type not in ["ID", "XPATH", "CSS_SELECTOR", "NAME"]:
self.logger.error(f"不支持的定位类型: {locator_type}")
return False
return True
def get_login_config(self, login_type: str = "back") -> Dict[str, tuple]:
"""
获取登录配置
Args:
login_type: 登录类型,"back"或"front"
Returns:
登录配置字典,格式:
{
"username": ("XPATH", "//input[@placeholder='...']", "admin@xty"),
"password": ("XPATH", "//input[@placeholder='...']", "password"),
"code": ("XPATH", "//input[@placeholder='...']", "csba")
}
Raises:
ValueError: 配置未加载或不支持的登录类型
"""
if self.system_config is None:
raise ValueError("系统配置未加载,请先调用load_system_config()")
system = self.system_config[0] # 使用第一个系统配置
# 根据登录类型获取配置
prefix = f"{login_type}_"
login_config = {}
for field in ["username", "password", "code"]:
config_key = f"{prefix}{field}"
if config_key not in system:
self.logger.error(f"缺少登录配置: {config_key}")
raise ValueError(f"缺少登录配置: {config_key}")
config_value = system[config_key]
# 格式:["定位类型", "定位值", "输入值"]
login_config[field] = (config_value[0], config_value[1], config_value[2])
return login_config
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, List, Tuple
from browser_operator import BrowserOperator
from utils.logger import get_logger
from utils.constants import WAIT_TIMEOUT, WAIT_SHORT
from config_manager import ConfigManager
class LoginHandler:
"""登录处理器"""
def __init__(self, browser: BrowserOperator, config_manager: ConfigManager):
"""
初始化登录处理器
Args:
browser: 浏览器操作器实例
config_manager: 配置管理器实例
"""
self.browser = browser
self.config_manager = config_manager
self.logger = get_logger("LoginHandler")
def login(self, url: str, login_type: str = "back") -> bool:
"""
执行登录操作(使用配置文件中的定位器)
Args:
url: 登录页面URL
login_type: 登录类型,"back"或"front"
Returns:
是否登录成功
"""
self.logger.info(f"开始登录流程,登录类型: {login_type}")
# 1. 获取登录配置
try:
login_config = self.config_manager.get_login_config(login_type)
except ValueError as e:
self.logger.error(f"获取登录配置失败: {e}")
return False
username_config = login_config["username"]
password_config = login_config["password"]
code_config = login_config["code"]
# 2. 打开登录页面
self.logger.info("正在打开登录页面...")
if not self.browser.open_page(url):
self.logger.error("打开登录页面失败")
return False
# 等待页面加载
import time
time.sleep(WAIT_SHORT)
# 3. 获取页面快照
snapshot = self.browser.take_snapshot()
# 4. 使用配置的定位器填写用户名
if not self._fill_input_by_locator(snapshot, username_config):
self.logger.error("填写用户名失败")
return False
# 5. 使用配置的定位器填写密码
if not self._fill_input_by_locator(snapshot, password_config):
self.logger.error("填写密码失败")
return False
# 6. 使用配置的定位器填写验证码(如果存在)
if code_config:
if not self._fill_input_by_locator(snapshot, code_config):
self.logger.warning("填写验证码失败,继续尝试登录")
else:
self.logger.info("无验证码配置,跳过验证码填写")
# 7. 查找并点击登录按钮
login_button_uid = self._find_login_button(snapshot)
if not login_button_uid:
self.logger.warning("快照中未找到登录按钮,尝试使用JavaScript直接查找并点击")
if not self._click_login_button_by_javascript():
self.logger.error("未找到登录按钮")
return False
else:
self.logger.info("正在点击登录按钮")
if not self.browser.click_element(login_button_uid):
self.logger.error("点击登录按钮失败")
return False
# 8. 等待登录完成
self.logger.info("等待登录完成...")
time.sleep(WAIT_SHORT)
# 9. 验证登录成功
if self.verify_login_success():
self.logger.info("登录成功")
return True
else:
self.logger.error("登录验证失败")
return False
def _fill_input_by_locator(self, snapshot: Dict, config: Tuple) -> bool:
"""
使用配置的定位器填写输入框
Args:
snapshot: 页面快照
config: 配置元组 (locator_type, locator_value, input_value)
Returns:
是否成功填写
"""
locator_type, locator_value, input_value = config
self.logger.debug(f"使用定位器填写输入: [{locator_type}, {locator_value}, {input_value}]")
# 查找元素
uid = self._find_element_by_locator(snapshot, locator_type, locator_value)
if not uid:
# 对于XPATH定位器,尝试使用JavaScript直接查找并填写
if locator_type == "XPATH":
self.logger.warning(f"快照中未找到元素,尝试使用JavaScript直接查找: {locator_value}")
return self._fill_input_by_javascript_xpath(locator_value, input_value)
self.logger.error(f"使用定位器未找到元素: {locator_value}")
return False
# 填写输入框
self.logger.info(f"正在填写值: {input_value}")
if not self.browser.fill_input(uid, input_value):
self.logger.error("填写失败")
return False
return True
def _fill_input_by_javascript_xpath(self, xpath: str, value: str) -> bool:
"""
使用JavaScript通过XPath直接填写输入框
Args:
xpath: XPath表达式
value: 要填写的值
Returns:
是否成功填写
"""
script = f'''
() => {{
const result = document.evaluate("{xpath}", document, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const element = result.singleNodeValue;
if (element) {{
element.value = "{value}";
element.dispatchEvent(new Event('input', {{ bubbles: true }}));
element.dispatchEvent(new Event('change', {{ bubbles: true }}));
return true;
}}
return false;
}}
'''
try:
result = self.browser.execute_script(script)
if result:
self.logger.info(f"使用JavaScript填写成功: {xpath}")
return True
else:
self.logger.error(f"JavaScript填写失败,未找到元素: {xpath}")
return False
except Exception as e:
self.logger.error(f"JavaScript填写异常: {e}")
return False
def _find_element_by_locator(self, snapshot: Dict, locator_type: str, locator_value: str) -> Optional[str]:
"""
根据定位器类型和值查找元素
Args:
snapshot: 页面快照
locator_type: 定位类型(XPATH/ID/CSS_SELECTOR等)
locator_value: 定位值
Returns:
元素的uid,未找到返回None
"""
elements = snapshot.get("elements", [])
for element in elements:
if locator_type == "XPATH":
# XPath定位需要通过JavaScript或其他方式匹配
# 这里简化处理:如果元素的某个属性包含定位值的部分内容
if self._match_xpath_locator(element, locator_value):
return element.get("uid")
elif locator_type == "ID":
if element.get("id") == locator_value:
return element.get("uid")
elif locator_type == "NAME":
if element.get("name") == locator_value:
return element.get("uid")
elif locator_type == "CSS_SELECTOR":
# CSS选择器匹配
if self._match_css_selector(element, locator_value):
return element.get("uid")
self.logger.warning(f"未找到匹配元素: [{locator_type}] {locator_value}")
return None
def _match_xpath_locator(self, element: Dict, xpath: str) -> bool:
"""
增强的XPath匹配(支持MCP快照格式)
支持的XPath格式:
- //input[@placeholder='value']
- //input[@id='value']
- //input[@name='value']
- //input[@type='value']
Args:
element: 元素字典(来自MCP快照)
xpath: XPath表达式
Returns:
是否匹配
"""
import re
# MCP快照中的元素格式:
# {
# "uid": "...",
# "role": "textbox",
# "name": "显示文本(可能是placeholder)",
# "attributes": {"placeholder": "...", "type": "...", "id": "...", ...}
# }
# 获取元素属性(MCP快照将属性存在attributes字段中)
element_attrs = element.get("attributes", {})
element_name = element.get("name", "")
# 匹配 placeholder 属性
if "@placeholder=" in xpath:
match = re.search(r"@placeholder='([^']+)'", xpath)
if match:
placeholder_value = match.group(1)
# 优先从attributes中获取placeholder
element_placeholder = element_attrs.get("placeholder", "")
if element_placeholder:
return placeholder_value == element_placeholder or placeholder_value in element_placeholder
# 备选:从name字段匹配(MCP快照中name可能显示placeholder文本)
if placeholder_value in element_name:
return True
# 匹配 id 属性
if "@id=" in xpath:
match = re.search(r"@id='([^']+)'", xpath)
if match:
id_value = match.group(1)
# 优先从attributes中获取id
element_id = element_attrs.get("id", "")
if element_id:
return element_id == id_value
# 备选:从顶层id字段获取
if element.get("id") == id_value:
return True
# 匹配 name 属性
if "@name=" in xpath:
match = re.search(r"@name='([^']+)'", xpath)
if match:
name_value = match.group(1)
# 优先从attributes中获取name
element_attr_name = element_attrs.get("name", "")
if element_attr_name:
return element_attr_name == name_value
# 备选:从顶层name字段获取
if element.get("name") == name_value:
return True
# 匹配 type 属性
if "@type=" in xpath:
match = re.search(r"@type='([^']+)'", xpath)
if match:
type_value = match.group(1)
element_type = element_attrs.get("type", "")
return element_type == type_value
return False
def _match_css_selector(self, element: Dict, selector: str) -> bool:
"""
简化的CSS选择器匹配
Args:
element: 元素字典
selector: CSS选择器
Returns:
是否匹配
"""
# 简化实现:支持class选择器
if selector.startswith(".") and "class" in element:
class_value = selector[1:]
element_classes = element.get("class", "")
if isinstance(element_classes, str):
return class_value in element_classes.split()
elif isinstance(element_classes, list):
return class_value in element_classes
return False
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
# 尝试通过XPATH查找
elements = snapshot.get("elements", [])
for element in elements:
role = element.get("role", "")
name = element.get("name", "")
if (role == "button" or element.get("tag") == "button") and \
any(keyword in name for keyword in ["登录", "提交"]):
return element.get("uid")
self.logger.warning("未找到登录按钮")
return None
# 旧的自动发现方法已移除,改用配置文件指定的定位器
def _click_login_button_by_javascript(self) -> bool:
"""
使用JavaScript查找并点击登录按钮
Returns:
是否成功点击
"""
# 常见的登录按钮XPATH
login_xpaths = [
"//button[contains(.,'登录')]",
"//button[contains(.,'登 录')]",
"//button[contains(.,'提交')]",
"//input[@type='submit']",
"//button[@type='submit']",
]
for xpath in login_xpaths:
script = f'''
() => {{
const result = document.evaluate("{xpath}", document, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const element = result.singleNodeValue;
if (element) {{
element.click();
return true;
}}
return false;
}}
'''
try:
result = self.browser.execute_script(script)
if result:
self.logger.info(f"使用JavaScript点击登录按钮成功: {xpath}")
return True
except Exception as e:
self.logger.debug(f"JavaScript点击失败: {xpath}, 错误: {e}")
self.logger.error("JavaScript未找到登录按钮")
return False
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",
login_type: str = "back"):
"""
初始化主控类
Args:
system_config_path: 系统配置文件路径
module_config_path: 模块配置文件路径
login_type: 登录类型,"front"或"back",默认为"back"
"""
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
self.login_type = login_type
# 设置文件日志
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, login_type = self._select_login_url_and_type(system_config, self.login_type)
# 3. 初始化浏览器
self._initialize_browser()
# 4. 登录系统
self._login_system(login_url, login_type)
# 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_and_type(self, system_config: Dict, login_type: str = "back") -> tuple[str, str]:
"""
选择登录URL和登录类型
Args:
system_config: 系统配置字典
login_type: 登录类型,"front"或"back"
Returns:
(登录URL, 登录类型) 元组
"""
if login_type == "front":
login_url = system_config.get("system_front_url")
self.logger.info(f"使用前台地址登录: {login_url}")
else: # back
login_url = system_config.get("system_back_url")
self.logger.info(f"使用后台地址登录: {login_url}")
return login_url, login_type
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, login_type: str) -> None:
"""
登录系统(使用配置文件中的定位器)
Args:
login_url: 登录URL
login_type: 登录类型,"front"或"back"
"""
self.logger.info("正在登录系统...")
# 使用配置管理器初始化登录处理器
self.login_handler = LoginHandler(self.browser, self.config_manager)
# 执行登录
if not self.login_handler.login(login_url, login_type):
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,
login_type=args.login_type
)
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)
......@@ -34,17 +34,20 @@
```json
[
{
"system_type": "后台系统",
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
},
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
"system_type": "前台系统",
"module_name": "会议室管理",
"module_name_son": "会议室列表",
"module_function": ["添加","编辑"]
}
]
```
```
- **注意**:`system_type`字段用于判断使用前台还是后台URL进行登录
3. 通过claude code+mcp,自动访问系统进行模块操作,并通过自动化测试用例JSON数据模板输出自动化测试用例。
- 自动化测试用例JSON数据模板格式示例:
......@@ -150,7 +153,7 @@
- 参数说明:
- name: 测试用例名称,命名格式为:{module_name}_{module_name_son}_{model_function},例如:区域管理_增值服务_添加。
- para: 测试步骤列表,每个步骤包含以下字段:
- page: page字段为当前模块功能操作的页面路由后缀。
- page: **当前模块功能操作的页面真实路由后缀**(非固定值),通过URL解析获取,例如:`"areaManagement/valueAddedService"`
- step: 操作步骤描述,例如:点击左侧【用户管理】。
- locator_type: 元素定位类型,例如:ID、XPATH、CSS_SELECTOR等。优先获取ID、XPATH,**不允许用UID**。
- locator_value: 元素定位值,例如://button[contains(.,'确定')],需要与locator_type一致。
......@@ -180,7 +183,9 @@
- `password`: 登录密码
- `code`: 验证码(固定值:csba)
**注意**:登录时需用户输入选择使用前台或后台URL。
**注意**:根据`module_config.json`中的`system_type`字段判断使用前台还是后台URL:
- `system_type`包含"后台""admin" 使用`system_back_url`
- `system_type`包含"前台"或其他 使用`system_front_url`
#### 3.1.2 module_config.json格式
- `module_name`: 一级菜单名称
......@@ -194,10 +199,16 @@
1. 先点击一级菜单(`module_name`)
2. 再点击二级菜单(`module_name_son`)
**注意**:导航到指定模块的步骤**必须记录到测试用例的`para`中**,作为测试步骤的前置操作。
#### 3.2.2 功能操作策略
- **添加操作**:填写通用固定测试值
- **编辑操作**:先自动创建测试数据 执行编辑 清理测试数据
- **删除操作**:先自动创建测试数据 执行删除 清理测试数据
- **编辑操作**:先自动创建测试数据 执行编辑 **清理测试数据**
- **注意**:清理操作步骤**需要记录到测试用例的`para`中**
- 如果清理失败,则记录提示步骤并标注"跳过清理"
- **删除操作**:先自动创建测试数据 执行删除 **清理测试数据**
- **注意**:清理操作步骤**需要记录到测试用例的`para`中**
- 如果清理失败,则记录提示步骤并标注"跳过清理"
#### 3.2.3 元素定位策略
采用**智能检测**方式,按以下优先级选择定位方式:
......@@ -209,12 +220,22 @@
#### 3.2.4 Page字段获取
通过**URL解析**获取当前功能所在页面的路由后缀。
**注意**:
- `page`字段**不是固定值**,必须从当前页面URL中提取真实路由
- 例如:当前URL为`https://192.168.5.44/#/areaManagement/valueAddedService`,则`page`字段应填写为`"areaManagement/valueAddedService"`
#### 3.2.5 预期结果生成
根据操作类型**自动生成**预期结果提示文本,例如:
- 添加操作 "添加成功"
- 编辑操作 "编辑成功"
- 删除操作 "删除成功"
#### 3.2.6 登录步骤处理
**重要**:生成的测试用例**不包含登录步骤**。
- 假设测试执行时系统已处于登录状态
- 登录操作由测试框架在执行测试用例前统一处理
- 测试用例直接从导航到目标模块开始记录
### 3.3 支持的元素类型(element_type)
#### 3.3.1 基础类型
......
# 操作手册 - 如何生成新模块的自动化测试用例
## 前置条件
### 1. 确保Chrome DevTools MCP工具已启动
```bash
# 检查MCP服务是否正常运行
# 确保 Claude Code 可以访问 chrome-devtools MCP 工具
```
### 2. 准备配置文件
确保以下配置文件已正确配置:
- `AuxiliaryTool/TestCaseGenerator/config/system_config.json` - 系统登录配置
- `AuxiliaryTool/TestCaseGenerator/config/module_config.json` - 模块配置
---
## 操作步骤
### 使用Claude Code对话式生成
#### Step 1: 更新模块配置文件
编辑 `module_config.json`,添加新模块配置:
```json
[
{
"system_type": "后台系统",
"module_name": "一级菜单名称",
"module_name_son": "二级菜单名称",
"module_function": ["添加", "编辑", "删除"]
}
]
```
**配置说明:**
- `system_type`: 包含"后台"或"admin"使用后台URL,否则使用前台URL
- `module_name`: 一级菜单名称(如:区域管理、用户管理)
- `module_name_son`: 二级菜单名称(如:增值服务、用户列表)
- `module_function`: 需要生成的功能类型,支持:
- `添加` - 新增数据操作
- `编辑` - 修改数据操作
- `删除` - 删除数据操作
#### Step 2: 在Claude Code对话中输入指令
```
请根据module_config.json和system_config.json生成自动化测试JSON数据
```
#### Step 3: Claude Code自动执行流程
Claude Code会通过MCP工具自动执行以下流程:
1. 打开浏览器并登录系统
2. 导航到指定模块(点击一级菜单 > 二级菜单)
3. 执行配置的功能操作(添加/编辑/删除)
4. 收集元素定位信息(使用XPATH等方式)
5. 生成测试用例JSON文件
#### Step 4: 查看生成的文件
生成的文件位于:
```
AuxiliaryTool/TestCaseGenerator/testcases/{模块名}_{子模块名}_{功能}.json
```
---
## 配置文件路径
**重要提示:** 目前配置文件位于以下位置,请确保路径正确:
```
AuxiliaryTool/TestCaseGenerator/
├── config/
│ ├── system_config.json # 系统登录配置
│ └── module_config.json # 模块配置(编辑此文件添加新模块)
└── testcases/ # 生成的测试用例输出目录
```
**如果config目录不存在,请先创建:**
```bash
mkdir -p "E:/GithubData/ubains-module-test/AuxiliaryTool/TestCaseGenerator/config"
```
---
## 配置文件详解
### 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" |
### module_config.json 格式
```json
[
{
"system_type": "后台系统",
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加", "编辑", "删除"]
},
{
"system_type": "前台系统",
"module_name": "会议室管理",
"module_name_son": "会议室列表",
"module_function": ["添加", "编辑"]
}
]
```
**注意:** 可以一次配置多个模块,程序会依次处理。
---
## 生成的测试用例JSON格式说明
### 完整结构
```json
{
"name": "{一级菜单}_{二级菜单}_{功能}",
"para": [
{
"page": "页面路由后缀",
"step": "操作步骤描述",
"locator_type": "定位类型",
"locator_value": "定位值",
"element_type": "元素类型",
"element_value": "操作值",
"expected_result": "预期结果"
}
],
"platform": "web",
"base_url": "系统基础URL"
}
```
### 字段说明
#### element_type 类型说明
| element_type | 说明 | element_value 填写 | expected_result 填写 |
|---------------|------|---------------------|----------------------|
| click | 点击按钮/链接 | 留空 `""` | 通常留空 |
| input | 文本输入框 | 填写输入内容 | 通常留空 |
| select | 下拉选择框 | 留空 `""` | 通常留空 |
| checkbox | 复选框/单选框 | 留空 `""` | 通常留空 |
| switch | 开关控件 | 留空 `""` | 通常留空 |
| getTips | 获取弹窗提示 | **留空** `""` | 填写预期提示文本 |
| getText | 获取列表文本 | **留空** `""` | 填写验证描述 |
#### locator_type 优先级
1. **ID** - 使用id属性(优先)
2. **NAME** - 使用name属性
3. **CLASS** - 使用class属性
4. **XPATH** - 使用XPATH表达式(备选)
5. **CSS_SELECTOR** - 使用CSS选择器(备选)
**注意:** 不允许使用UID作为定位类型。
---
## 常见问题处理
### Q1: 浏览器已运行错误
**错误信息:** `The browser is already running...`
**解决方案:**
这个错误通常不影响Claude Code执行,因为Claude Code会自动连接到现有的浏览器实例。如果出现问题,可以:
```bash
# Windows - 关闭所有Chrome进程(谨慎使用)
taskkill /F /IM chrome.exe
```
然后重新启动Claude Code对话。
### Q2: 登录失败
**可能原因:**
- 网络不通,无法访问系统地址
- 账号密码错误
- 验证码已更改
**解决方案:**
- 检查网络连接
- 更新system_config.json中的账号密码
- 确认验证码是否为"csba"
### Q3: 找不到菜单元素
**可能原因:**
- 菜单名称配置错误
- 页面加载未完成
**解决方案:**
- 确认module_name和module_name_son与实际页面完全一致
- 增加等待时间(修改utils/constants.py中的WAIT_TIMEOUT)
### Q4: 元素定位失败
**可能原因:**
- 页面结构变化
- 元素在iframe中
**解决方案:**
- 使用Chrome DevTools检查页面结构
- 考虑使用iframe处理
---
## 输出目录结构
```
AuxiliaryTool/TestCaseGenerator/
├── config/
│ ├── system_config.json # 系统配置(登录信息)
│ └── module_config.json # 模块配置(要生成的模块)
└── testcases/ # 生成的测试用例输出目录
├── 区域管理_增值服务_添加.json
├── 区域管理_增值服务_编辑.json
└── 区域管理_增值服务_删除.json
```
**注意:** 如果config目录不存在,请先创建:
```bash
mkdir -p "E:/GithubData/ubains-module-test/AuxiliaryTool/TestCaseGenerator/config"
```
---
## 扩展:批量生成多个模块
### 一次生成多个模块的配置示例
```json
[
{
"system_type": "后台系统",
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加", "编辑", "删除"]
},
{
"system_type": "后台系统",
"module_name": "区域管理",
"module_name_son": "区域列表",
"module_function": ["添加", "编辑", "删除"]
},
{
"system_type": "后台系统",
"module_name": "用户管理",
"module_name_son": "用户列表",
"module_function": ["添加", "编辑"]
}
]
```
程序会依次处理每个模块配置,生成对应的测试用例文件。
---
## 注意事项
1. **数据清理**:编辑和删除操作会创建测试数据,测试完成后需要手动清理
2. **页面等待**:如果网络较慢,可能需要增加等待时间
3. **编码问题**:确保JSON文件使用UTF-8编码保存
4. **浏览器版本**:建议使用最新版Chrome浏览器
---
## 附录:快速开始模板
### Step 1: 确保配置文件存在
**创建config目录(如果不存在):**
```bash
mkdir -p "E:/GithubData/ubains-module-test/AuxiliaryTool/TestCaseGenerator/config"
```
**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
[{
"system_type": "后台系统",
"module_name": "一级菜单",
"module_name_son": "二级菜单",
"module_function": ["添加"]
}]
```
### Step 2: 在Claude Code中执行
直接输入指令:
```
请根据module_config.json和system_config.json生成自动化测试JSON数据
```
### Step 3: 查看结果
生成的文件位于:
```
AuxiliaryTool/TestCaseGenerator/testcases/
```
---
## 总结
**目前唯一支持的方式:** 通过Claude Code对话式生成
**核心优势:**
- 无需编写代码
- 自动处理浏览器操作
- 智能识别元素定位
- 自动生成标准JSON格式
**下次使用时:** 只需更新 `module_config.json`,然后在Claude Code中输入指令即可!
---
*文档版本:v1.1*
*更新日期:2026-03-06*
*更新说明:移除命令行方式,仅保留Claude Code对话式生成方式*
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论