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

docs(prd): 更新AI用例完善需求文档并重构缺失检测与生成逻辑

- 重命名需求文档文件名添加_PRD_前缀
- 新增用例设计章节详述等价类划分、边界值分析等测试设计方法
- 添加日志打印功能便于调试AI服务调用情况
- 集成TEST_DESIGN_METHODS常量到AI提示词模板中
- 重构GapDetector类使用AIServiceManager支持API和CLI模式
- 重构CaseGenerator类按功能点、场景、边界分别生成测试用例
- 更新实施计划章节编号并添加测试用例设计方案
- 新增AI调用逻辑优化需求文档说明分步调用方案
- 删除已解决的XPATH定位器问题记录文档
上级 04f2212c
......@@ -120,6 +120,9 @@ class AIServiceManager:
"""
model = model or AI_MODEL
# 打印提示词到控制台(方便调试)
self._log_prompt(prompt, model, "ask_json")
if self.mode == "cli":
# CLI模式:适配器会自动处理JSON解析
return self.cli_adapter.ask_json(prompt, model)
......@@ -147,6 +150,9 @@ class AIServiceManager:
"""
model = model or AI_MODEL
# 打印提示词到控制台(方便调试)
self._log_prompt(prompt, model, "ask_json_array")
if self.mode == "cli":
# CLI模式:适配器会自动处理JSON数组解析
return self.cli_adapter.ask_json_array(prompt, model)
......@@ -161,6 +167,42 @@ class AIServiceManager:
result_text = response.content[0].text
return self._parse_json_array(result_text)
def _log_prompt(self, prompt: str, model: str, method: str) -> None:
"""
打印提示词到控制台(用于调试和日志记录)
Args:
prompt: 提示词内容
model: 模型名称
method: 调用方法名称
"""
# 打印分隔线
print("=" * 80)
print(f"[AI服务调用] 模型: {model} | 方法: {method} | 模式: {self.mode}")
print("=" * 80)
# 如果提示词过长,进行摘要打印
if len(prompt) > 2000:
print(f"[提示词摘要] 总长度: {len(prompt)} 字符")
print("-" * 80)
# 打印前500字符
print("[前500字符]")
print(prompt[:500])
print("\n... (省略中间内容) ...\n")
# 打印后500字符
print(f"[后500字符] (从 {len(prompt)-500} 开始)")
print(prompt[-500:])
else:
print("[完整提示词]")
print(prompt)
print("=" * 80)
print() # 空行分隔
# 同时记录到日志
logger.info("[AI服务调用] 模型=%s, 方法=%s, 模式=%s, 提示词长度=%d",
model, method, self.mode, len(prompt))
def _extract_json(self, text: str) -> str:
"""
从文本中提取JSON字符串
......
......@@ -14,7 +14,7 @@ from datetime import datetime
from src.config import (
AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE,
TEST_CASE_COLUMNS, CASE_LEVELS, DEFAULT_CASE_LEVEL,
DEFAULT_TEST_FREQUENCY, TEST_FREQUENCIES
DEFAULT_TEST_FREQUENCY, TEST_FREQUENCIES, TEST_DESIGN_METHODS
)
logger = getLogger(__name__)
......@@ -162,11 +162,13 @@ class CaseGenerator:
- 功能模块:{gap.get('功能模块', '')}
- 功能编号:{gap.get('功能编号', '')}
- 功能描述:{gap.get('功能描述', '')}
- 建议设计方法:{gap.get('设计方法', '场景法')}
【要求】
1. 生成详细的测试步骤
2. 预期结果要明确具体
3. 考虑正常流程和基本验证
4. 应用等价类划分和边界值分析方法
请输出JSON格式的测试用例:
"""
......@@ -176,13 +178,18 @@ class CaseGenerator:
prompt += f"""
注意:
{TEST_DESIGN_METHODS}
【额外要求】
1. 必须输出合法的JSON格式
2. 用例编号格式:TC-{gap.get('功能模块', 'TEST')[:4].upper()}-{seq:03d}
3. 用例等级:{gap.get('优先级', DEFAULT_CASE_LEVEL)}
4. STEP要包含详细的操作步骤,每步一行
5. JSON字段填写:暂不涉及自动化测试
6. 测试频次:{DEFAULT_TEST_FREQUENCY}
7. 根据功能特点选择合适的测试设计方法(等价类划分、边界值分析、场景法等)
8. 对于输入框类功能,必须包含边界值测试
9. 对于权限类功能,必须包含不同角色的权限验证
"""
return self._call_ai_for_case(prompt, gap.get("功能描述", ""))
......@@ -216,6 +223,7 @@ class CaseGenerator:
- 功能描述:{gap.get('功能描述', '')}
- 已有场景:{gap.get('已有场景', '')}
- 缺失场景:{gap.get('缺失场景', '')}
- 场景类型:{gap.get('场景类型', '异常场景')}
【要求】
1. 专门针对缺失场景设计测试步骤
......@@ -230,12 +238,37 @@ class CaseGenerator:
prompt += f"""
注意:
{TEST_DESIGN_METHODS}
【场景设计指导】
根据场景类型应用相应的设计方法:
1. 异常场景设计:
- 输入无效数据(空值、超长值、特殊字符)
- 模拟操作失败(网络中断、服务不可用)
- 验证错误提示信息
2. 边界场景设计:
- 测试边界值(最大值、最小值)
- 测试边界±1的值
- 测试空值和null值
3. 并发场景设计:
- 模拟多用户同时操作
- 验证数据一致性
- 验证锁机制
4. 权限场景设计:
- 不同角色权限验证
- 越权访问测试
- 权限边界测试
【额外要求】
1. 必须输出合法的JSON格式
2. 用例名称:{gap.get('建议用例名称', f'测试{gap.get("功能描述", "")}{gap.get("缺失场景", "")}')}
3. 用例等级:{gap.get('优先级', DEFAULT_CASE_LEVEL)}
4. STEP要详细描述如何触发该场景
5. 预期结果要描述该场景的预期行为
5. 预期结果要描述该场景的预期行为(成功或失败,错误提示等)
"""
return self._call_ai_for_case(prompt, gap.get("缺失场景", ""))
......@@ -269,6 +302,7 @@ class CaseGenerator:
- 功能描述:{gap.get('功能描述', '')}
- 已有边界:{gap.get('已有边界', '')}
- 缺失边界:{gap.get('缺失边界', '')}
- 边界类型:{gap.get('边界类型', '边界值')}
【要求】
1. 专门针对该边界条件设计测试步骤
......@@ -283,12 +317,46 @@ class CaseGenerator:
prompt += f"""
注意:
{TEST_DESIGN_METHODS}
【边界值设计指导】
根据边界类型应用边界值分析方法:
1. 数值型边界:
- 上边界:最大值
- 上边界+1:超过最大值
- 上边界-1:接近最大值
- 下边界:最小值
- 下边界+1:接近最小值
- 下边界-1:低于最小值
- 零值(如适用)
2. 字符串边界:
- 空字符串:''
- 最大长度:边界值
- 超长字符串:最大长度+1
- 特殊字符:SQL注入、XSS攻击字符
3. 列表/集合边界:
- 空列表:[]
- 单元素:[item]
- 最大数量:边界值
- 超过最大值:最大值+1
4. 日期时间边界:
- 最小日期:系统支持的最小日期
- 最大日期:系统支持的最大日期
- 边界日期±1天
- 无效日期:2月30日、13月等
【额外要求】
1. 必须输出合法的JSON格式
2. 用例名称:{gap.get('建议用例名称', f'测试{gap.get("功能描述", "")}{gap.get("缺失边界", "")}')}
3. 用例等级:{gap.get('优先级', DEFAULT_CASE_LEVEL)}
4. STEP要详细描述如何测试该边界条件
5. 预期结果要描述边界值和附近值的预期行为
4. STEP要详细描述如何测试该边界条件(包括测试数据的具体值)
5. 预期结果要描述边界值和附近值的预期行为(通过或失败,错误提示等)
6. 对于数值边界,测试数据要包含具体的数值
7. 对于字符串边界,测试数据要包含具体的字符串长度示例
"""
return self._call_ai_for_case(prompt, gap.get("缺失边界", ""))
......@@ -451,3 +519,131 @@ def format_case_number(module: str, seq: int) -> str:
module_short = "TEST"
return f"TC-{module_short}-{seq:03d}"
# ==================== v2版本:优化后的精简提示词方法 ====================
def generate_cases_v2(self, gaps: Dict, existing_cases: List[Dict]) -> List[Dict]:
"""
使用精简提示词逐个生成测试用例(步骤3)
优化说明:
- 每个提示词长度控制在600字符以内
- 逐个生成用例,避免一次性处理过多信息
- 简化测试设计方法说明
Args:
gaps: 缺失检测结果
existing_cases: 现有测试用例列表
Returns:
新生成的测试用例列表
"""
logger.info("开始逐个生成测试用例(v2版本 - 精简提示词)...")
new_cases = []
next_seq = self._get_next_sequence(existing_cases)
# 遍历所有缺失类型
for gap_type, gap_list in gaps.items():
for gap in gap_list:
try:
# 为每个缺失生成一个测试用例
case = self._generate_case_v2(gap, gap_type, next_seq)
if case:
new_cases.append(case)
next_seq += 1
except Exception as e:
logger.error("生成测试用例失败: %s, 错误: %s",
gap.get('功能描述', ''), e)
logger.info("生成测试用例完成: %d 条", len(new_cases))
return new_cases
def _generate_case_v2(self, gap: Dict, gap_type: str, seq: int) -> Optional[Dict]:
"""
使用精简提示词生成单个测试用例
Args:
gap: 缺失信息
gap_type: 缺失类型(功能点缺失/场景覆盖不足/边界条件不足)
seq: 序列号
Returns:
测试用例字典,失败返回None
"""
prompt = self._build_step3_prompt_v2(gap, gap_type, seq)
logger.info("步骤3提示词长度: %d 字符", len(prompt))
try:
# 使用AI服务管理器调用
case = self.ai_service.ask_json(prompt, AI_MODEL)
# 类型检查:确保返回的是字典
if isinstance(case, list):
if len(case) > 0 and isinstance(case[0], dict):
case = case[0]
else:
logger.error("AI返回的数组格式不符合预期")
return None
if not isinstance(case, dict):
logger.error("AI返回的不是字典对象: %s", type(case))
return None
# 设置默认值
if not case.get("测试频次"):
case["测试频次"] = DEFAULT_TEST_FREQUENCY
if not case.get("JSON"):
case["JSON"] = "暂不涉及"
# 设置序列号
case["序列号"] = seq
return case
except Exception as e:
logger.error("生成测试用例失败: %s", e)
logger.debug("错误详情", exc_info=True)
return None
def _build_step3_prompt_v2(self, gap: Dict, gap_type: str, seq: int) -> str:
"""
构建步骤3提示词:生成单个测试用例(精简版本)
提示词长度控制在600字符以内
"""
prompt = f"""你是一个测试专家。为以下缺失生成测试用例。
【缺失信息】
- 类型:{gap_type}
- 功能描述:{gap.get('功能描述', '')}
"""
# 根据缺失类型添加特定信息
if gap_type == "场景覆盖不足":
prompt += f"- 已有场景:{gap.get('已有场景', '')}\n"
prompt += f"- 缺失场景:{gap.get('缺失场景', '')}\n"
elif gap_type == "边界条件不足":
prompt += f"- 已有边界:{gap.get('已有边界', '')}\n"
prompt += f"- 缺失边界:{gap.get('缺失边界', '')}\n"
prompt += f"""
【测试设计方法】
等价类划分:有效等价类、无效等价类(空值、特殊字符、超长值)
边界值分析:上边界、下边界、边界±1
场景法:基本流、备选流
错误推测:异常输入、并发冲突
【输出格式】严格遵循JSON:
{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
【要求】
1. 用例编号格式:TC-{gap.get('功能模块', 'TEST')[:4].upper()}-{seq:03d}
2. 用例等级:{gap.get('优先级', 'P1')}
3. STEP包含详细操作步骤,每步一行
4. JSON字段填写:暂不涉及
5. 测试频次:每版本
6. 根据缺失类型应用相应的测试设计方法
7. 边界条件测试需包含具体的边界值数据
"""
return prompt
......@@ -117,6 +117,64 @@ TEST_FREQUENCIES = ["每次", "每日", "每周", "每月", "每版本", "一次
# 默认测试频次
DEFAULT_TEST_FREQUENCY = "每版本"
# ==================== 测试用例设计方法 ====================
# 测试设计方法提示词模板
TEST_DESIGN_METHODS = """
在生成测试用例时,请应用以下测试设计方法:
【功能性测试设计方法】
1. 等价类划分:识别有效等价类和无效等价类,为每个等价类设计代表性用例
- 有效等价类:符合需求规格说明的输入数据
- 无效等价类:违反需求规格说明的输入数据(空值、特殊字符、超长字符串、非法格式等)
- 示例:输入框限制1-100的整数,有效类为"1≤x≤100",无效类包括"x<1""x>100""非整数""空值"
2. 边界值分析:针对有范围限制的输入,测试边界值及边界±1的值
- 上边界、上边界+1、上边界-1
- 下边界、下边界+1、下边界-1
- 示例:测试年龄输入框,需覆盖0、1、100、101等值
3. 场景法:设计完整的用户操作流程,包括基本流和备选流
- 基本流:正常操作流程
- 备选流:异常处理流程
- 示例:登录→浏览商品→下单→支付→订单确认
4. 错误推测法:基于经验设计特殊字符、空值、异常输入等测试场景
- 空指针场景
- 并发冲突
- 数据溢出
- 特殊字符处理(SQL注入、XSS攻击测试字符)
- 网络异常
- 权限越界
【非功能性测试设计】
1. 性能测试:设计大数据量、高并发场景的测试用例
- 响应时间测试:测量单个操作的响应时间
- 并发测试:模拟多用户同时操作
- 压力测试:测试系统极限承载能力
2. 兼容性测试:考虑不同浏览器、设备的兼容性
- 浏览器:Chrome、Firefox、Edge、Safari
- 设备分辨率:1920x1080、1366x768、移动端适配
- 操作系统:Windows、macOS、Linux
3. 安全测试:设计SQL注入、XSS攻击、越权访问等安全测试用例
- SQL注入:输入特殊SQL语句尝试注入
- XSS攻击:输入脚本代码测试防护
- 敏感数据:检查密码、token等是否加密存储
- 权限控制:验证越权访问是否被阻止
4. 异常场景:设计网络中断、服务不可用等异常情况
- 网络中断场景
- 服务不可用场景
- 数据库连接失败场景
【测试用例优先级判定标准】
- P0(最高):核心功能、关键业务路径、安全相关
- P1(高):重要功能、常用场景、性能相关
- P2(中):辅助功能、一般场景、兼容性相关
- P3(低):边缘功能、异常场景、UI美化相关
"""
def ensure_directories() -> None:
"""
......
......@@ -9,7 +9,7 @@ import json
from typing import List, Dict, Optional
from logging import getLogger
from src.config import AI_MODEL, COVERAGE_TARGET
from src.config import AI_MODEL, COVERAGE_TARGET, TEST_DESIGN_METHODS
logger = getLogger(__name__)
......@@ -183,7 +183,8 @@ class GapDetector:
"功能编号": "F-001",
"功能描述": "缺失的功能描述",
"建议用例名称": "测试XXX功能",
"优先级": "P1"
"优先级": "P1",
"设计方法": "等价类划分/场景法/错误推测法"
}
],
"场景覆盖不足": [
......@@ -192,7 +193,8 @@ class GapDetector:
"已有场景": "正常场景",
"缺失场景": "异常场景:XXX",
"建议用例名称": "测试XXX异常场景",
"优先级": "P2"
"优先级": "P2",
"场景类型": "异常场景/边界场景/并发场景/权限场景"
}
],
"边界条件不足": [
......@@ -201,23 +203,28 @@ class GapDetector:
"已有边界": "常规值",
"缺失边界": "边界值:XXX",
"建议用例名称": "测试XXX边界条件",
"优先级": "P2"
"优先级": "P2",
"边界类型": "上边界/下边界/空值/超长值"
}
]
}
注意:
""" + TEST_DESIGN_METHODS + """
【额外要求】
1. 必须输出合法的JSON格式
2. 只输出JSON对象,不要有其他说明文字
3. 优先级分为:P0(最高)、P1、P2、P3(最低)
4. 优先识别P0和P1级别的缺失
5. 考虑以下常见场景:
5. 应用上述测试设计方法进行缺失分析
6. 特别关注以下场景类型:
- 正常场景:基本功能验证
- 异常场景:错误处理、异常输入
- 边界条件:最大值、最小值、空值、null
- 并发场景:多用户同时操作
- 权限场景:不同角色权限验证
- 性能场景:大数据量、长时间操作
- 安全场景:SQL注入、XSS攻击、越权访问
"""
return prompt
......@@ -366,3 +373,113 @@ def estimate_additional_cases(gaps: Dict) -> Dict:
logger.info("估算需新增测试用例: %d 条", total)
return result
# ==================== v2版本:优化后的精简提示词方法 ====================
def detect_gaps_v2(self, function_points: List[Dict],
test_cases: List[Dict]) -> Dict:
"""
使用精简提示词检测缺失(步骤2)
优化说明:
- 提示词长度控制在800字符以内
- 测试用例使用摘要(前20条)
- 简化测试设计方法说明
Args:
function_points: 功能点列表
test_cases: 测试用例列表
Returns:
缺失检测结果,包含:
- 功能点缺失: 缺失的功能点列表
- 场景覆盖不足: 场景覆盖不足的列表
- 边界条件不足: 边界条件不足的列表
"""
logger.info("开始检测测试用例缺失(v2版本 - 精简提示词)...")
logger.info(" 功能点数: %d", len(function_points))
logger.info(" 测试用例数: %d", len(test_cases))
prompt = self._build_step2_prompt_v2(function_points, test_cases)
logger.info("步骤2提示词长度: %d 字符", len(prompt))
try:
# 使用AI服务管理器调用
result = self.ai_service.ask_json(prompt, AI_MODEL)
# 处理返回结果
gaps = {"功能点缺失": [], "场景覆盖不足": [], "边界条件不足": []}
if isinstance(result, dict):
# 直接是字典格式,合并结果
for key in gaps:
if key in result and isinstance(result[key], list):
gaps[key] = result[key]
logger.info("AI检测完成:")
logger.info(" 功能点缺失: %d 项", len(gaps["功能点缺失"]))
logger.info(" 场景覆盖不足: %d 项", len(gaps["场景覆盖不足"]))
logger.info(" 边界条件不足: %d 项", len(gaps["边界条件不足"]))
return gaps
except Exception as e:
logger.error("AI调用失败: %s", e)
logger.debug("错误详情", exc_info=True)
return {"功能点缺失": [], "场景覆盖不足": [], "边界条件不足": []}
def _build_step2_prompt_v2(self, function_points: List[Dict],
test_cases: List[Dict]) -> str:
"""
构建步骤2提示词:检测缺失(精简版本)
提示词长度控制在800字符以内
"""
# 格式化测试用例摘要
cases_summary = self._summarize_test_cases_v2(test_cases, max_items=20)
prompt = f"""你是一个测试专家。对比功能点和现有用例,找出缺失。
【测试设计方法】
等价类划分、边界值分析、场景法、错误推测法、因果图判定表、性能测试、安全测试、兼容性测试
【功能点】
{json.dumps(function_points, ensure_ascii=False)}
【现有测试用例摘要】(共{len(test_cases)}条)
{cases_summary}
【输出格式】严格遵循JSON:
{{
"功能点缺失": [{{"功能模块": "", "功能描述": "", "建议用例名称": "", "优先级": "P1"}}],
"场景覆盖不足": [{{"功能描述": "", "已有场景": "", "缺失场景": "", "建议用例名称": "", "优先级": "P2"}}],
"边界条件不足": [{{"功能描述": "", "已有边界": "", "缺失边界": "", "建议用例名称": "", "优先级": "P2"}}]
}}
【要求】
1. 优先级:P0(最高)、P1、P2、P3(最低)
2. 只输出JSON对象,不要其他文字
3. 识别缺失时应用上述测试设计方法
"""
return prompt
def _summarize_test_cases_v2(self, test_cases: List[Dict], max_items: int = 20) -> str:
"""
生成测试用例摘要
Args:
test_cases: 测试用例列表
max_items: 最多显示的用例数量
Returns:
用例摘要字符串
"""
lines = []
for i, case in enumerate(test_cases[:max_items], 1):
lines.append(f"{i}. {case.get('用例名称', '')} - {case.get('功能描述', '')}")
result = "\n".join(lines)
if len(test_cases) > max_items:
result += f"\n... (还有{len(test_cases)-max_items}条)"
return result
......@@ -423,3 +423,182 @@ def save_enhanced_test_cases(existing_cases: list, new_cases: list,
if __name__ == "__main__":
sys.exit(main())
# ==================== v2版本:优化后的分步调用流程 ====================
def main_v2(use_cli: bool = False) -> int:
"""
主函数 - 使用优化后的分步调用流程
优化说明:
- 采用A+B组合方案:分步调用 + 精简提示词
- 每个步骤提示词控制在1000字符以内
- 失败可重试单个步骤
Args:
use_cli: 是否强制使用CLI模式
Returns:
退出码(0表示成功,非0表示失败)
"""
try:
# 配置日志
setup_logging()
ensure_directories()
logger.info("=" * 60)
logger.info("AI完善测试用例工具 启动(优化版本 v2)")
logger.info("版本: 2.0.0 - 分步调用+精简提示词")
logger.info("=" * 60)
# 检查AI服务模式
api_key = os.environ.get(API_KEY_ENV)
service_mode = get_ai_service_mode()
# 命令行参数优先级最高
if use_cli:
service_mode = "cli"
# 根据服务模式初始化
if service_mode == "api":
if not api_key or not api_key.strip():
logger.error("API模式需要ANTHROPIC_API_KEY环境变量")
return 1
use_cli = False
masked_key = api_key[:8] + "..." if len(api_key) > 8 else "***"
logger.info("使用Anthropic API模式,密钥: %s", masked_key)
else:
use_cli = True
logger.info("使用Claude CLI模式")
# 验证输入
validate_inputs()
# 1. 读取文档
logger.info("")
logger.info("【步骤1】读取文档")
logger.info("-" * 60)
reader = DocumentReader()
docs = reader.read_all_documents(REQUIREMENTS_DIR, PRD_DIR, TEST_CASE_FILE)
requirements = docs["requirements"]
prds = docs["prds"]
test_cases = docs["test_cases"]
logger.info("需求文档: %d 份", len(requirements))
logger.info("PRD文档: %d 份", len(prds))
logger.info("测试用例: %d 条", len(test_cases))
# ========== 步骤1:提取功能点 ==========
logger.info("")
logger.info("【步骤2】提取功能点(v2 - 精简提示词 ~500字符)")
logger.info("-" * 60)
analyzer = TestCaseAnalyzer(api_key=api_key, use_cli=use_cli)
function_points = analyzer.extract_function_points_v2(requirements, prds)
if not function_points:
logger.warning("未提取到功能点,请检查需求文档内容")
# 继续执行,但使用空列表
logger.info("提取到功能点: %d 个", len(function_points))
# ========== 步骤2:检测缺失 ==========
logger.info("")
logger.info("【步骤3】检测缺失(v2 - 精简提示词 ~800字符)")
logger.info("-" * 60)
detector = GapDetector(api_key=api_key, use_cli=use_cli)
gaps = detector.detect_gaps_v2(function_points, test_cases)
logger.info("功能点缺失: %d 项", len(gaps.get("功能点缺失", [])))
logger.info("场景覆盖不足: %d 项", len(gaps.get("场景覆盖不足", [])))
logger.info("边界条件不足: %d 项", len(gaps.get("边界条件不足", [])))
# 估算新增用例数量
estimate = estimate_additional_cases(gaps)
logger.info("估算需新增用例: %d 条", estimate.get("总计", 0))
# ========== 步骤3:生成用例 ==========
logger.info("")
logger.info("【步骤4】生成测试用例(v2 - 精简提示词 ~600字符/条)")
logger.info("-" * 60)
generator = CaseGenerator(api_key=api_key, use_cli=use_cli)
new_cases = generator.generate_cases_v2(gaps, test_cases)
logger.info("生成测试用例: %d 条", len(new_cases))
# ========== 后续处理 ==========
# 去重
logger.info("")
logger.info("【步骤5】去重处理")
logger.info("-" * 60)
deduplicator = Deduplicator()
dedup_result = deduplicator.deduplicate(test_cases, new_cases)
logger.info("去重后保留: %d 条", dedup_result["summary"]["unique_cases"])
# 保存完善后的测试用例
logger.info("")
logger.info("【步骤6】保存完善后的测试用例")
logger.info("-" * 60)
save_enhanced_test_cases(test_cases, dedup_result["unique_cases"], OUTPUT_TEST_CASE)
# 生成差异性报告
logger.info("")
logger.info("【步骤7】生成差异性报告")
logger.info("-" * 60)
reporter = ReportGenerator()
# 计算覆盖率
coverage = analyzer.analyze_coverage(function_points, test_cases)
reporter.generate_report(gaps, dedup_result, coverage, OUTPUT_REPORT, len(test_cases))
# 输出总结
logger.info("")
logger.info("=" * 60)
logger.info("处理完成!")
logger.info("=" * 60)
logger.info("【输入】")
logger.info(" 需求文档: %d 份", len(requirements))
logger.info(" PRD文档: %d 份", len(prds))
logger.info(" 原始用例: %d 条", len(test_cases))
logger.info("")
logger.info("【输出】")
logger.info(" 完善后的测试用例: %s", OUTPUT_TEST_CASE)
logger.info(" 差异性报告: %s", OUTPUT_REPORT)
logger.info("")
logger.info("【统计】")
logger.info(" 覆盖率: %.1f%%", coverage.get("coverage_rate", 0) * 100)
logger.info(" 新增用例: %d 条(去重后)", dedup_result["summary"]["unique_cases"])
logger.info(" 功能点缺失: %d 项", len(gaps.get("功能点缺失", [])))
logger.info(" 场景缺失: %d 项", len(gaps.get("场景覆盖不足", [])))
logger.info(" 边界缺失: %d 项", len(gaps.get("边界条件不足", [])))
logger.info("=" * 60)
return 0
except KeyboardInterrupt:
logger.info("")
logger.info("用户中断操作")
return 130
except FileNotFoundError as e:
logger.error("文件未找到: %s", e)
return 1
except PermissionError as e:
logger.error("权限错误: %s", e)
logger.error("请检查文件是否被其他程序占用")
return 1
except Exception as e:
logger.error("处理失败: %s", e)
import traceback
logger.debug(traceback.format_exc())
return 1
......@@ -413,6 +413,101 @@ class TestCaseAnalyzer:
return result
# ==================== v2版本:优化后的精简提示词方法 ====================
def extract_function_points_v2(self, requirements: List[Dict],
prds: List[Dict]) -> List[Dict]:
"""
使用精简提示词提取功能点(步骤1)
优化说明:
- 提示词长度控制在500字符以内
- 需求文档内容限制在300字符以内
- 简化输出格式要求
Args:
requirements: 需求文档列表
prds: PRD文档列表
Returns:
功能点列表
"""
logger.info("开始提取功能点(v2版本 - 精简提示词)...")
prompt = self._build_step1_prompt_v2(requirements, prds)
logger.info("步骤1提示词长度: %d 字符", len(prompt))
try:
# 使用AI服务管理器
from src.ai_service_manager import AIServiceManager
ai_service = AIServiceManager(api_key=os.environ.get(API_KEY_ENV))
function_points = ai_service.ask_json_array(prompt)
logger.info("功能点提取完成,共 %d 个", len(function_points))
return function_points
except Exception as e:
logger.error("提取功能点失败: %s", e)
logger.debug("错误详情", exc_info=True)
return []
def _build_step1_prompt_v2(self, requirements: List[Dict],
prds: List[Dict]) -> str:
"""
构建步骤1提示词:提取功能点(精简版本)
提示词长度控制在500字符以内
"""
# 格式化需求文档,限制长度
req_text = self._format_requirements_v2(requirements, prds, max_chars=300)
prompt = f"""你是一个测试专家。分析需求文档,提取功能点列表。
【需求文档】
{req_text}
【要求】
1. 识别所有功能模块
2. 每个功能包含:功能模块、功能描述
3. 输出JSON数组格式
【输出格式】
[{{"功能模块": "模块名", "功能描述": "功能描述"}}]
"""
return prompt
def _format_requirements_v2(self, requirements: List[Dict],
prds: List[Dict], max_chars: int = 300) -> str:
"""
格式化需求文档,控制长度
Args:
requirements: 需求文档列表
prds: PRD文档列表
max_chars: 最大字符数
Returns:
格式化后的文档字符串
"""
parts = []
total_chars = 0
# 合并需求和PRD
all_docs = requirements + prds
for doc in all_docs:
# 取前200字符作为摘要
content_preview = doc['content'][:200]
if len(doc['content']) > 200:
content_preview += "..."
content = f"【{doc['filename']}】{content_preview}"
parts.append(content)
total_chars += len(content)
if total_chars >= max_chars:
break
return "\n".join(parts)
def analyze_by_module(self, test_cases: List[Dict]) -> Dict[str, Dict]:
"""
按功能模块分析测试用例
......
......@@ -69,6 +69,7 @@ cd AuxiliaryTool/DocumentAutoOptimizationCalibration
2. 安装依赖:
```bash
pip install -r requirements.txt
pip install pywin32
```
---
......
......@@ -199,7 +199,7 @@ def _translate_header_footer_part(header_footer, engine: str,
translated_filename: str = None,
original_filename: str = None) -> int:
"""
翻译页眉或页脚中的段落内容
翻译页眉或页脚中的段落、表格和文本框内容
Args:
header_footer: 页眉或页脚对象
......@@ -208,15 +208,21 @@ def _translate_header_footer_part(header_footer, engine: str,
original_filename: 原文件名(用于判断是否需要替换)
Returns:
成功翻译的段落数量
成功翻译的内容数量(段落+表格单元格+文本框)
"""
translated_count = 0
table_cell_count = 0
textbox_count = 0
# 判断是否为页眉(通过检查类型或属性)
# 定义命名空间(用于文本框处理)
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
'v': 'urn:schemas-microsoft-com:vml'
}
# 判断是否为页眉
is_header = False
try:
# 检查是否为header类型
from docx.oxml.ns import qn
if hasattr(header_footer, '_element'):
parent = header_footer._element.getparent()
if parent is not None:
......@@ -224,6 +230,7 @@ def _translate_header_footer_part(header_footer, engine: str,
except Exception:
pass
# 处理页眉页脚中的段落
for paragraph in header_footer.paragraphs:
if not paragraph.text.strip():
continue
......@@ -282,6 +289,87 @@ def _translate_header_footer_part(header_footer, engine: str,
except Exception as e:
logger.warning("页眉页脚翻译异常: %s", e)
# 处理页眉页脚中的表格
for table in header_footer.tables:
for row in table.rows:
for cell in row.cells:
if _translate_table_cell(cell, engine):
table_cell_count += 1
if table_cell_count > 0:
logger.info("页眉页脚表格翻译: %d 个单元格", table_cell_count)
translated_count += table_cell_count
# 处理页眉页脚中的文本框
for paragraph in header_footer.paragraphs:
# 查找段落中的文本框(新版格式)
for drawing in paragraph._element.findall('.//w:drawing', namespaces):
for textbox in drawing.findall('.//w:txbxContent', namespaces):
# 处理文本框内的段落
for p_element in textbox.findall('.//w:p', namespaces):
text_elements = p_element.findall('.//w:t', namespaces)
if text_elements:
original_text = ''.join([t.text for t in text_elements if t.text])
if original_text.strip() and is_translatable(original_text):
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
for i, t in enumerate(text_elements):
if i == 0:
t.text = translated_text
else:
t.text = ""
textbox_count += 1
logger.info("页眉页脚文本框翻译: %s -> %s",
original_text[:30], translated_text[:30])
except Exception as e:
logger.warning("页眉页脚文本框翻译异常: %s", e)
# 查找旧版格式的文本框
for shape in paragraph._element.findall('.//v:shape', namespaces):
textbox = shape.find('.//v:textbox', namespaces)
if textbox is not None:
txbx_content = textbox.find('.//w:txbxContent', namespaces)
if txbx_content is not None:
for p_element in txbx_content.findall('.//w:p', namespaces):
text_elements = p_element.findall('.//w:t', namespaces)
if text_elements:
original_text = ''.join([t.text for t in text_elements if t.text])
if original_text.strip() and is_translatable(original_text):
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
for i, t in enumerate(text_elements):
if i == 0:
t.text = translated_text
else:
t.text = ""
textbox_count += 1
logger.info("页眉页脚文本框翻译(旧版): %s -> %s",
original_text[:30], translated_text[:30])
except Exception as e:
logger.warning("页眉页脚文本框翻译异常(旧版): %s", e)
if textbox_count > 0:
logger.info("页眉页脚文本框翻译: %d 个", textbox_count)
translated_count += textbox_count
return translated_count
......@@ -325,6 +413,368 @@ def _preserve_hyperlinks(paragraph, engine: str) -> None:
pass
def _find_all_textboxes(document):
"""
查找文档中所有文本框
在Word文档中,文本框存储在 w:txbxContent 元素中。
该函数遍历文档XML,查找所有文本框并返回其中的段落。
Args:
document: Document对象
Yields:
包含文本框段落的Element对象
"""
# 定义命名空间
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
'v': 'urn:schemas-microsoft-com:vml',
'wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'
}
# 遍历文档正文的所有段落
for paragraph in document.paragraphs:
# 查找段落中的所有drawing元素(新版格式)
for drawing in paragraph._element.findall('.//w:drawing', namespaces):
# 查找文本框内容
for textbox in drawing.findall('.//w:txbxContent', namespaces):
yield textbox
# 查找段落中的所有legacy shape元素(旧版Word格式)
for shape in paragraph._element.findall('.//v:shape', namespaces):
# 旧版shape可能包含v:textbox
textbox = shape.find('.//v:textbox', namespaces)
if textbox is not None:
# 获取textbox内容
txbx_content = textbox.find('.//w:txbxContent', namespaces)
if txbx_content is not None:
yield txbx_content
def _translate_textboxes(document, engine: str) -> int:
"""
翻译文档中所有文本框内容
Args:
document: Document对象
engine: 翻译引擎
Returns:
成功翻译的文本框段落数量
"""
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
'v': 'urn:schemas-microsoft-com:vml'
}
translated_count = 0
textbox_index = 0
for textbox_content in _find_all_textboxes(document):
textbox_index += 1
logger.debug("处理文本框 #%d", textbox_index)
# 获取文本框内的所有段落
paragraphs = textbox_content.findall('.//w:p', namespaces)
for p_element in paragraphs:
# 获取原始文本
text_elements = p_element.findall('.//w:t', namespaces)
if not text_elements:
continue
# 获取原始文本
original_text = ''.join([t.text for t in text_elements if t.text])
if not original_text.strip():
continue
# 检查是否需要翻译
if not is_translatable(original_text):
logger.debug("文本框跳过(无需翻译): %s", original_text[:30])
continue
# 执行翻译
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
# 替换文本内容 - 保留第一个文本元素并设置翻译内容
for i, t in enumerate(text_elements):
if i == 0:
t.text = translated_text
else:
# 清除其他文本元素
t.text = ""
translated_count += 1
logger.info("文本框翻译[%d]: %s -> %s",
textbox_index, original_text[:30], translated_text[:30])
else:
logger.debug("文本框翻译失败或未翻译: %s", original_text[:30])
except Exception as e:
logger.warning("文本框段落翻译异常: %s", e)
logger.info("文本框翻译完成: 总数=%d, 翻译=%d", textbox_index, translated_count)
return translated_count
def _find_toc_paragraphs(document):
"""
查找文档中的目录段落
Word目录通常包含特定特征:
1. 包含域代码指令 TOC ...
2. 或包含 "目录" 标题
3. 后面跟随的是标题条目
Args:
document: Document对象
Returns:
(toc_start_index, toc_end_index) - 目录开始和结束段落索引
"""
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
}
toc_start = -1
toc_end = -1
in_toc = False
for i, paragraph in enumerate(document.paragraphs):
text = paragraph.text.strip()
# 检测目录开始
if not in_toc:
# 方法1:查找包含"目录"的标题
if text in ['目录', '目 录', 'Contents', 'Table of Contents']:
if 'Heading' in paragraph.style.name or '标题' in paragraph.style.name:
toc_start = i
in_toc = True
continue
# 方法2:通过XML查找TOC域代码
for fldSimple in paragraph._element.findall('.//w:fldSimple', namespaces):
instr = fldSimple.get(f'{{{namespaces["w"]}}}instr')
if instr and 'TOC' in instr:
toc_start = i
in_toc = True
break
# 方法3:查找嵌套的域代码
for fldChar in paragraph._element.findall('.//w:fldChar', namespaces):
fldChar_type = fldChar.get(f'{{{namespaces["w"]}}}fldCharType')
if fldChar_type == 'begin':
# 查找后续的instrText
for sibling in paragraph._element.itersiblings():
instr_text = sibling.find('.//w:instrText', namespaces)
if instr_text is not None and instr_text.text and 'TOC' in instr_text.text:
toc_start = i
in_toc = True
break
# 检测目录结束
if in_toc:
# 目录通常在遇到下一个一级标题或分隔线时结束
if i > toc_start + 1: # 至少有目录标题
# 检查是否是下一个一级标题
if _is_heading_paragraph(paragraph) == 1:
toc_end = i
break
# 检查是否是分隔线(多个连续的=或-)
if text and all(c in '=-_—' for c in text):
toc_end = i
break
# 如果没有找到明确的结束位置,假设目录在50个段落内
if toc_start >= 0 and toc_end < 0:
toc_end = min(toc_start + 50, len(document.paragraphs))
return toc_start, toc_end
def _translate_toc_manual(document, engine: str) -> int:
"""
手动翻译目录内容
直接翻译目录段落中的文字,保持原有格式
Args:
document: Document对象
engine: 翻译引擎
Returns:
成功翻译的目录条目数量
"""
import re
toc_start, toc_end = _find_toc_paragraphs(document)
if toc_start < 0:
logger.info("未检测到目录,跳过目录翻译")
return 0
logger.info("检测到目录位置: 段落 %d - %d", toc_start + 1, toc_end + 1)
translated_count = 0
# 翻译目录中的段落
for i in range(toc_start, min(toc_end + 1, len(document.paragraphs))):
paragraph = document.paragraphs[i]
original_text = paragraph.text.strip()
if not original_text:
continue
# 跳过目录标题本身
if original_text in ['目录', '目 录', 'Contents', 'Table of Contents']:
continue
# 检查是否需要翻译
if not is_translatable(original_text):
continue
# 目录条目通常包含页码,格式如 "标题....................1"
# 需要分离标题和页码
toc_pattern = r'^(.+?)[\s\u3000\.。—_-]{1,}(\d+)$'
match = re.match(toc_pattern, original_text)
if match:
title_text = match.group(1).strip()
page_number = match.group(2)
# 只翻译标题部分
if is_translatable(title_text):
try:
translated_title = translate_text(
title_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_title and translated_title != title_text:
# 重建目录条目:翻译后的标题 + 分隔符 + 页码
# 尝试提取原有的分隔符
separator = ''
for j in range(len(title_text), len(original_text)):
if original_text[j].isdigit():
break
separator += original_text[j]
if not separator:
separator = ' ' + '.' * 20 + ' '
new_text = translated_title + separator + page_number
# 清除原有runs
for run in paragraph.runs:
r = run._element
r.getparent().remove(r)
# 添加新文本
new_run = paragraph.add_run(new_text)
translated_count += 1
logger.debug("目录条目翻译: %s -> %s", title_text[:30], translated_title[:30])
except Exception as e:
logger.warning("目录条目翻译异常: %s", e)
else:
# 不符合目录条目格式的,尝试直接翻译
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
# 清除原有runs
for run in paragraph.runs:
r = run._element
r.getparent().remove(r)
# 添加新文本
new_run = paragraph.add_run(translated_text)
translated_count += 1
logger.debug("目录内容翻译: %s -> %s", original_text[:30], translated_text[:30])
except Exception as e:
logger.warning("目录内容翻译异常: %s", e)
logger.info("目录手动翻译完成: 翻译了 %d 个条目", translated_count)
return translated_count
def _update_fields_via_win32com(doc_path: str) -> bool:
"""
使用win32com自动更新Word文档中的所有域
此功能需要:
1. Windows操作系统
2. 安装Microsoft Word
3. 安装pywin32库
Args:
doc_path: Word文档路径
Returns:
是否成功更新
"""
try:
import win32com.client
logger.info("使用win32com更新文档域...")
# 启动Word应用
word = win32com.client.Dispatch("Word.Application")
word.Visible = False # 不显示Word窗口
word.DisplayAlerts = 0 # 禁用警告
try:
# 打开文档
doc = word.Documents.Open(str(doc_path))
# 更新所有域
doc.Fields.Update()
# 更新目录(如果有)
try:
doc.TablesOfContents(1).Update()
logger.info("目录已更新")
except Exception:
pass
# 保存并关闭
doc.Save()
doc.Close()
logger.info("win32com域更新完成")
return True
finally:
# 退出Word应用
word.Quit()
except ImportError:
logger.debug("未安装pywin32库,跳过win32com域更新")
return False
except Exception as e:
logger.warning("win32com更新域失败: %s", e)
return False
def internationalize_document(
input_path: str,
output_path: Optional[str] = None,
......@@ -417,10 +867,27 @@ def internationalize_document(
logger.info("页眉页脚翻译完成: 数量=%d", header_footer_count)
# 处理文本框翻译
textbox_count = _translate_textboxes(doc, engine)
# 保存翻译后的文档
doc.save(str(output_file))
logger.info("国际化转换完成: %s", output_file)
# 处理目录更新
# 方案A:尝试使用win32com自动更新域
win32com_success = _update_fields_via_win32com(output_file)
if not win32com_success:
# 方案B:手动翻译目录
logger.info("win32com不可用,使用手动目录翻译...")
doc = Document(output_file)
toc_count = _translate_toc_manual(doc, engine)
if toc_count > 0:
doc.save(str(output_file))
logger.info("目录手动翻译完成: %d 个条目", toc_count)
else:
logger.info("提示:请在Word中打开文档后,右键点击目录并选择'更新域'以更新目录内容")
return str(output_file)
[
{
"序号": 1,
"功能模块": "通用模块",
"功能类别": "异常场景",
"用例编号": "TC-001",
"功能描述": "权限组管理接口文档",
"用例等级": "中",
"功能编号": "",
"用例名称": "权限组管理接口文档",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. ## 1. 分页查询权限组; 2. **接口描述**:根据公司编号、权限组名称、启用状态等条件进行分页查询,支持模糊匹配权限组名称,按创建时间倒序排列; 3. **接口描述**:执行物理删除操作,同时级联删除部门关联、角色关联、用户关联表中的数据,使用事务确保删除操作的原子性。基础权限组(is_base=1)不可删除。; 4. ## 7. 查询绑定关系; 5. **接口描述**:查询权限组已绑定的关联对象,支持按类型筛选或返回全部类型; 6. ## 8. 查询用户最终权限",
"JSON": "",
"预期结果": "**请求头**:需要携带有效的 Token 认证; **响应示例**:; **响应示例**(成功):; **响应示例**(包含基础权限组):; > - departmentIds、roleIds、userIds 至少需要传入一种类型(可为空数组); **响应示例**(不传relationType,返回全部):",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 2,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-002",
"功能描述": "**直接绑定**:查询sys_permission_group_user中user_id=用户ID的权限组",
"用例等级": "中",
"功能编号": "1",
"用例名称": "**直接绑定**:查询sys_permission_group_user中user_id=用户ID的权限组",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "**直接绑定**:查询sys_permission_group_user中user_id=用户ID的权限组 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 3,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-003",
"功能描述": "**部门绑定**:查询sys_permission_group_department中department_id=用户所属部门ID的权限组",
"用例等级": "中",
"功能编号": "2",
"用例名称": "**部门绑定**:查询sys_permission_group_department中department_id=用户所属部门ID的权限组",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "**部门绑定**:查询sys_permission_group_department中department_id=用户所属部门ID的权限组 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 4,
"功能模块": "通用模块",
"功能类别": "异常场景",
"用例编号": "TC-004",
"功能描述": "**角色绑定**:查询sys_permission_group_role中role_id=用户所属角色ID的权限组",
"用例等级": "中",
"功能编号": "3",
"用例名称": "**角色绑定**:查询sys_permission_group_role中role_id=用户所属角色ID的权限组",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. | A_SELECT_EMPTY | 查询结果为空 |; 2. *文档生成时间:2026年1月12日*",
"JSON": "",
"预期结果": "**响应示例**:; | 200 | 成功 |; | 值 | 说明 | 对应表 |",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
}
]
\ No newline at end of file
[
{
"序号": 5,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-005",
"功能描述": "权限管理页面新增开发",
"用例等级": "中",
"功能编号": "",
"用例名称": "权限管理页面新增开发",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限管理页面新增开发 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 6,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-006",
"功能描述": "权限管理页面地址: `src\\views\\Backend\\Admin\\PermissionManage\\index.vue`",
"用例等级": "中",
"功能编号": "1",
"用例名称": "权限管理页面地址: `src\\views\\Backend\\Admin\\PermissionManage\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限管理页面地址: `src\\views\\Backend\\Admin\\PermissionManage\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 7,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-007",
"功能描述": "相关API文档地址:`Docs\\Api\\API_权限组管理接口文档.md`",
"用例等级": "中",
"功能编号": "2",
"用例名称": "相关API文档地址:`Docs\\Api\\API_权限组管理接口文档.md`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "相关API文档地址:`Docs\\Api\\API_权限组管理接口文档.md` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 8,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-008",
"功能描述": "新增/修改权限组弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\AddEditDialog\\index.vue`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "新增/修改权限组弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\AddEditDialog\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "新增/修改权限组弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\AddEditDialog\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 9,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-009",
"功能描述": "权限绑定弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\BindDialog\\index.vue`",
"用例等级": "中",
"功能编号": "4",
"用例名称": "权限绑定弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\BindDialog\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限绑定弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\BindDialog\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 10,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-010",
"功能描述": "自定义权限控制指令v-permission地址:`src\\utils\\permission.js`",
"用例等级": "中",
"功能编号": "5",
"用例名称": "自定义权限控制指令v-permission地址:`src\\utils\\permission.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制指令v-permission地址:`src\\utils\\permission.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 11,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-011",
"功能描述": "✅ 在权限管理页面遵循`Docs\\PRD\\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\\router.js`注册路由",
"用例等级": "中",
"功能编号": "1",
"用例名称": "✅ 在权限管理页面遵循`Docs\\PRD\\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\\router.js`注册路由",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 在权限管理页面遵循`Docs\\PRD\\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\\router.js`注册路由 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 12,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-012",
"功能描述": "✅ 参考页面`src\\views\\Backend\\Admin\\Role\\index.vue`的设计在初始化后的权限管理页新增功能",
"用例等级": "中",
"功能编号": "2",
"用例名称": "✅ 参考页面`src\\views\\Backend\\Admin\\Role\\index.vue`的设计在初始化后的权限管理页新增功能",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 参考页面`src\\views\\Backend\\Admin\\Role\\index.vue`的设计在初始化后的权限管理页新增功能 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 13,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-013",
"功能描述": "✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\\views\\Backend\\Admin\\PermissionManage\\components`中,注意数据的传输,并优化一下样式",
"用例等级": "中",
"功能编号": "3",
"用例名称": "✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\\views\\Backend\\Admin\\PermissionManage\\components`中,注意数据的传输,并优化一下样式",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\\views\\Backend\\Admin\\PermissionManage\\components`中,注意数据的传输,并优化一下样式 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 14,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-014",
"功能描述": "✅ 实现权限组添加权限",
"用例等级": "中",
"功能编号": "4",
"用例名称": "✅ 实现权限组添加权限",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 实现权限组添加权限 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 15,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-015",
"功能描述": "✅ 在新增/修改权限组弹窗继续实现任务4的要求",
"用例等级": "中",
"功能编号": "5",
"用例名称": "✅ 在新增/修改权限组弹窗继续实现任务4的要求",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 在新增/修改权限组弹窗继续实现任务4的要求 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 16,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-016",
"功能描述": "✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现",
"用例等级": "中",
"功能编号": "6",
"用例名称": "✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 17,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-017",
"功能描述": "✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\\views\\Backend\\Admin\\PermissionManage\\components\\PermissionConfig\\index.vue`的permissionList替换为接口请求到的真实数据",
"用例等级": "中",
"功能编号": "7",
"用例名称": "✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\\views\\Backend\\Admin\\PermissionManage\\components\\PermissionConfig\\index.vue`的permissionList替换为接口请求到的真实数据",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\\views\\Backend\\Admin\\PermissionManage\\components\\PermissionConfig\\index.vue`的permissionList替换为接口请求到的真实数据 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 18,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-018",
"功能描述": "✅ 重构权限绑定弹窗及补充功能实现",
"用例等级": "中",
"功能编号": "8",
"用例名称": "✅ 重构权限绑定弹窗及补充功能实现",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 重构权限绑定弹窗及补充功能实现 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 19,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-019",
"功能描述": "数据结构调整",
"用例等级": "中",
"功能编号": "9",
"用例名称": "数据结构调整",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "数据结构调整 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 20,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-020",
"功能描述": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"用例等级": "高",
"功能编号": "1",
"用例名称": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 21,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-021",
"功能描述": "支持模糊搜索,支持基础的增删改查",
"用例等级": "中",
"功能编号": "2",
"用例名称": "支持模糊搜索,支持基础的增删改查",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "支持模糊搜索,支持基础的增删改查 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 22,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-022",
"功能描述": "el-table的主要显示字段获取参考相关API文档的\"分页查询权限组\"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定",
"用例等级": "中",
"功能编号": "3",
"用例名称": "el-table的主要显示字段获取参考相关API文档的\"分页查询权限组\"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "el-table的主要显示字段获取参考相关API文档的\"分页查询权限组\"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 23,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-023",
"功能描述": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"用例等级": "高",
"功能编号": "1",
"用例名称": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 24,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-024",
"功能描述": "我在`src\\constant\\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示",
"用例等级": "中",
"功能编号": "2",
"用例名称": "我在`src\\constant\\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "我在`src\\constant\\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 25,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-025",
"功能描述": "文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 26,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-026",
"功能描述": "页面显示:要同时显示名称和勾选框,比如",
"用例等级": "中",
"功能编号": "4",
"用例名称": "页面显示:要同时显示名称和勾选框,比如",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "页面显示:要同时显示名称和勾选框,比如 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 27,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-027",
"功能描述": "弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能",
"用例等级": "中",
"功能编号": "1",
"用例名称": "弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 28,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-028",
"功能描述": "页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max",
"用例等级": "中",
"功能编号": "2",
"用例名称": "页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 29,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-029",
"功能描述": "参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取",
"用例等级": "中",
"功能编号": "3",
"用例名称": "参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 30,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-030",
"功能描述": "参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取",
"用例等级": "中",
"功能编号": "4",
"用例名称": "参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 31,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-031",
"功能描述": "参考`@src/views/Backend/Account/User/index.vue:263-273 `和`@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取",
"用例等级": "中",
"功能编号": "5",
"用例名称": "参考`@src/views/Backend/Account/User/index.vue:263-273 `和`@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "参考`@src/views/Backend/Account/User/index.vue:263-273 `和`@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 32,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-032",
"功能描述": "根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定",
"用例等级": "中",
"功能编号": "6",
"用例名称": "根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 33,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-033",
"功能描述": "当前接口的数据结构",
"用例等级": "中",
"功能编号": "1",
"用例名称": "当前接口的数据结构",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "当涉及到接口请求时,需要严格遵循`Docs\\PRD\\_PRD_规范文档_接口请求.md`规范",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
}
]
\ No newline at end of file
[
{
"序号": 34,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-034",
"功能描述": "自定义权限控制指令实现",
"用例等级": "中",
"功能编号": "",
"用例名称": "自定义权限控制指令实现",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制指令实现 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 35,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-035",
"功能描述": "办公室管理页面地址: `src\\views\\Backend\\MeetingRoom\\OfficeManage\\index.vue`",
"用例等级": "中",
"功能编号": "1",
"用例名称": "办公室管理页面地址: `src\\views\\Backend\\MeetingRoom\\OfficeManage\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "办公室管理页面地址: `src\\views\\Backend\\MeetingRoom\\OfficeManage\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 36,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-036",
"功能描述": "自定义权限控制指令地址:`src\\utils\\permission.js`",
"用例等级": "中",
"功能编号": "2",
"用例名称": "自定义权限控制指令地址:`src\\utils\\permission.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制指令地址:`src\\utils\\permission.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 37,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-037",
"功能描述": "权限数据结构`src\\constant\\permissionList.js`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "权限数据结构`src\\constant\\permissionList.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限数据结构`src\\constant\\permissionList.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 38,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-038",
"功能描述": "权限控制指令通过`src\\main.js`挂载成`v-permission`使用",
"用例等级": "中",
"功能编号": "4",
"用例名称": "权限控制指令通过`src\\main.js`挂载成`v-permission`使用",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限控制指令通过`src\\main.js`挂载成`v-permission`使用 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 39,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-039",
"功能描述": "实现自定义权限控制指令",
"用例等级": "中",
"功能编号": "1",
"用例名称": "实现自定义权限控制指令",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "实现自定义权限控制指令 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 40,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-040",
"功能描述": "先将权限数据结构存储在localStorage的userPermission中,模拟用户已经获取到当前的拥有权限",
"用例等级": "中",
"功能编号": "1",
"用例名称": "先将权限数据结构存储在localStorage的userPermission中,模拟用户已经获取到当前的拥有权限",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "先将权限数据结构存储在localStorage的userPermission中,模拟用户已经获取到当前的拥有权限 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 41,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-041",
"功能描述": "我在办公室管理的页面line:16添加了v-permission=\"'functionType_82.create'\",帮我根据判断实现:如果userPermission的functionType_82.create为1,则显示按钮,并将functionType_82.create作为id赋予给按钮,为0或者不存在则将按钮移出dom",
"用例等级": "中",
"功能编号": "2",
"用例名称": "我在办公室管理的页面line:16添加了v-permission=\"'functionType_82.create'\",帮我根据判断实现:如果userPermission的functionType_82.create为1,则显示按钮,并将functionType_82.create作为id赋予给按钮,为0或者不存在则将按钮移出dom",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "我在办公室管理的页面line:16添加了v-permission=\"'functionType_82.create'\",帮我根据判断实现:如果userPermission的functionType_82.create为1,则显示按钮,并将functionType_82.create作为id赋予给按钮,为0或者不存在则将按钮移出dom 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 42,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-042",
"功能描述": "自定义权限控制的生命周期等问题可以参考`E:\\Project\\ubains-unified-platform\\src\\common\\permission.js`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "自定义权限控制的生命周期等问题可以参考`E:\\Project\\ubains-unified-platform\\src\\common\\permission.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制的生命周期等问题可以参考`E:\\Project\\ubains-unified-platform\\src\\common\\permission.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
}
]
\ No newline at end of file
[
{
"序号": 1, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-001",
"功能描述": "正常登录成功", "用例等级": "高", "功能编号": "",
"用例名称": "正常账号密码登录成功",
"预置条件": "1. 系统已部署且MQTT/Redis可用; 2. 存在有效账号A",
"操作步骤": "1. 打开登录页; 2. 输入账号A正确用户名密码; 3. 点击“登录”; 4. 观察接口返回和页面跳转",
"JSON": "",
"预期结果": "接口返回token/companyNumber/userId/userIp; 页面跳首页; Redis写入3类token键; 前端缓存token",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 2, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-002",
"功能描述": "登录后建立MQTT并设置遗嘱", "用例等级": "高", "功能编号": "",
"用例名称": "登录成功后建立MQTT连接",
"预置条件": "1. 用例1已成功, 账号A在首页; 2. 可查看开发者工具或抓包",
"操作步骤": "1. 登录成功后打开开发者工具; 2. 找到MQTT连接; 3. 展开will配置并核对参数",
"JSON": "",
"预期结果": "MQTT连接成功; clientId格式正确; clean=false; will topic/payload/qos/retain符合设计",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 3, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-003",
"功能描述": "本机同浏览器重复登录", "用例等级": "中", "功能编号": "",
"用例名称": "本机同浏览器再次登录同账号",
"预置条件": "账号A已在当前浏览器登录",
"操作步骤": "1. 新建同浏览器标签页; 2. 输入账号A和正确密码; 3. 点击“登录”; 4. 观察返回结果",
"JSON": "",
"预期结果": "按策略提示“本机已登录”或清旧token生成新token, 无系统异常",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": "根据最终策略确认"
},
{
"序号": 4, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-004",
"功能描述": "异地登录触发唯一端控制", "用例等级": "高", "功能编号": "",
"用例名称": "终端B登录成功并挤掉终端A",
"预置条件": "终端A浏览器已登录账号A; 终端B未登录",
"操作步骤": "1. 终端B打开登录页输入账号A和正确密码; 2. 点击“登录”; 3. 登录成功后访问受保护页面; 4. 在终端A刷新页面或访问接口",
"JSON": "",
"预期结果": "终端B正常使用; 后端清理终端A旧token并发MQTT挤下线通知; 终端A接口401/403或弹窗被挤下线",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 5, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-005",
"功能描述": "被挤端弹窗提示并返回登录页", "用例等级": "高", "功能编号": "",
"用例名称": "被挤下线端处理逻辑",
"预置条件": "终端A停留在业务页; 终端B用账号A刚完成登录",
"操作步骤": "1. 观察终端A是否弹出被挤下线提示; 2. 在终端A点击“确定”; 3. 重新执行并点击“×”; 4. 再次在终端A访问接口或刷新页面",
"JSON": "",
"预期结果": "终端A弹窗提示; 点击“确定/×”后清token、断开MQTT并跳登录页; 未点击时接口401/403并被拦截到登录页",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 6, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-006",
"功能描述": "正常登出不触发遗嘱", "用例等级": "高", "功能编号": "",
"用例名称": "正常登出流程验证",
"预置条件": "账号A已登录, MQTT连接正常",
"操作步骤": "1. 在页面点击“退出登录”; 2. 观察前端MQTT断开日志; 3. 查看/logout响应; 4. 确认无遗嘱消息; 5. 再访问业务页",
"JSON": "POST /logout",
"预期结果": "前端先正常断开MQTT且无遗嘱; /logout成功; 前端清token并回登录页; 后端未收到WILL",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 7, "功能模块": "登录与MQTT", "功能类别": "异常场景", "用例编号": "LZ-LOGIN-007",
"功能描述": "浏览器关闭触发遗嘱清理token", "用例等级": "高", "功能编号": "",
"用例名称": "浏览器关闭异常断开",
"预置条件": "账号A已登录, MQTT正常",
"操作步骤": "1. 登录成功后停留在业务页; 2. 直接关闭浏览器窗口或结束进程; 3. 查看后端是否收到WILL; 4. 查看Redis中token; 5. 使用旧token访问接口",
"JSON": "",
"预期结果": "浏览器关闭导致MQTT异常断开并发布WILL; 后端清理3类token; 旧token访问接口返回401/403",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 8, "功能模块": "登录与MQTT", "功能类别": "异常场景", "用例编号": "LZ-LOGIN-008",
"功能描述": "网络中断触发遗嘱并尝试重连", "用例等级": "中", "功能编号": "",
"用例名称": "弱网下MQTT异常断开与重连",
"预置条件": "账号A已登录, MQTT正常",
"操作步骤": "1. 登录成功后立即断网; 2. 观察前端close事件日志; 3. 查看后端是否收到WILL并清理token; 4. 恢复网络; 5. 观察重连日志与结果",
"JSON": "",
"预期结果": "断网触发异常断开; 后端清token; 前端按指数退避多次重连; token失效时最终提示重新登录",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 9, "功能模块": "登录与MQTT", "功能类别": "异常场景", "用例编号": "LZ-LOGIN-009",
"功能描述": "弱网多次重连失败达到最大次数", "用例等级": "中", "功能编号": "",
"用例名称": "MQTT重连超过最大次数处理",
"预置条件": "前端配置maxRetries=5; 账号A已登录",
"操作步骤": "1. 登录成功后断网并保持异常; 2. 观察重连次数与间隔; 3. 等待超过5次; 4. 观察前端提示; 5. 确认不再继续重连",
"JSON": "",
"预期结果": "最多重试5次且间隔指数退避; 超过次数后停止重连并弹出网络异常提示, 引导重新登录",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 10, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-010",
"功能描述": "刷新页面后的登录态与MQTT重建", "用例等级": "中", "功能编号": "",
"用例名称": "刷新页面保持/失效逻辑",
"预置条件": "账号A已登录并停留在业务页",
"操作步骤": "1. 在业务页按F5刷新; 2. 观察是否仍在业务页; 3. 查看是否重新建立MQTT并设置遗嘱; 4. 再次访问业务接口",
"JSON": "",
"预期结果": "token有效则刷新后重建MQTT并可继续使用; token已清则接口401/403并跳登录页",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 11, "功能模块": "登录与MQTT", "功能类别": "功能测试", "用例编号": "LZ-LOGIN-011",
"功能描述": "多标签页共享/同步登出", "用例等级": "中", "功能编号": "",
"用例名称": "多标签页BroadcastChannel同步登出",
"预置条件": "同一浏览器两个标签页均登录账号A",
"操作步骤": "1. 在标签页1点击“退出登录”; 2. 观察标签页1退回登录页; 3. 切到标签页2观察是否自动退回登录页; 4. 在任一标签页访问接口验证需重新登录",
"JSON": "",
"预期结果": "标签页1登出并发送LOGOUT; 标签页2收到后断开MQTT、清token并回登录页; 所有标签页都需重新登录",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 12, "功能模块": "登录与MQTT", "功能类别": "安全/健壮", "用例编号": "LZ-LOGIN-012",
"功能描述": "遗嘱payload type异常不应清理token", "用例等级": "中", "功能编号": "",
"用例名称": "非WILL类型payload处理",
"预置条件": "后端MQTT订阅正常, Redis中有合法token",
"操作步骤": "1. 用MQTT客户端向will topic发送payload {type:'OTHER',token:'fake',ip:'1.1.1.1'}; 2. 观察后端日志与Redis中token变化",
"JSON": "",
"预期结果": "后端识别type≠WILL,仅记录告警, 不清理token, 服务正常",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 13, "功能模块": "登录与MQTT", "功能类别": "安全/健壮", "用例编号": "LZ-LOGIN-013",
"功能描述": "遗嘱payload字段缺失/格式错误处理", "用例等级": "中", "功能编号": "",
"用例名称": "异常payload健壮性验证",
"预置条件": "后端MQTT订阅正常, 可查看错误日志",
"操作步骤": "1. 发送payload {type:'WILL',ip:'1.1.1.1'}; 2. 再发送非JSON文本“bad payload”; 3. 观察错误日志; 4. 验证后续合法遗嘱仍能正常处理",
"JSON": "",
"预期结果": "异常payload被容错处理并记录日志; 不清理token; 订阅不中断; 后续合法遗嘱正常",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 14, "功能模块": "登录与MQTT", "功能类别": "安全/鉴权", "用例编号": "LZ-LOGIN-014",
"功能描述": "使用被清理的旧token访问接口", "用例等级": "高", "功能编号": "",
"用例名称": "旧token失效验证",
"预置条件": "账号A已被挤下线或触发遗嘱, token已清理; 已记录旧token值",
"操作步骤": "1. 在Postman/浏览器设置Authorization: Bearer 旧token; 2. 调用受保护接口; 3. 观察响应; 4. 在前端刷新业务页观察处理",
"JSON": "",
"预期结果": "接口统一返回401/403; 前端清token并提示重新登录; 不存在仍可用旧token的接口",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
},
{
"序号": 15, "功能模块": "登录与MQTT", "功能类别": "运维可观测", "用例编号": "LZ-LOGIN-015",
"功能描述": "关键链路日志完整性检查", "用例等级": "中", "功能编号": "",
"用例名称": "登录/登出/遗嘱/挤下线日志检查",
"预置条件": "系统开启业务日志, 有权限查看",
"操作步骤": "1. 执行一次正常登录→登出; 2. 执行一次异地挤下线; 3. 执行一次浏览器关闭触发遗嘱; 4. 在日志平台查看对应记录",
"JSON": "",
"预期结果": "登录成功、MQTT连接、挤下线通知、遗嘱触发、token清理等关键步骤均有日志, 异常场景有告警记录",
"测试结果": "", "测试结论": "", "日志截屏": "", "备注": ""
}
]
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
根据模板用例 Excel,在同一个工作簿中新建 Sheet,并从 PRD(Markdown) 自动生成的 JSON 用例写入。
需求对齐(Docs/PRD/_PRD_根据PRD生成用例.md):
- PRD 文档统一路径:Docs/开发PRD目录(仓库实际目录名可能为 Docs/PRD,下面做了兼容)
- 执行脚本时通过交互型输入需要生成测试用例的PRD文档编号
- 若多选,则生成在同一个 sheet 表中;sheet 名 = 交互输入的“测试用例名称”
- 生成的测试用例文件名:交互输入名称 + 时间戳
"""
import os
import re
import json
import argparse
from copy import copy
from datetime import datetime
from dataclasses import dataclass
from typing import List, Dict
from openpyxl import load_workbook
from openpyxl.styles import Alignment
# ===== 1) 路径与表头 =====
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
# ✅ 兼容两种目录:Docs/开发PRD(PRD里写的) & Docs/PRD(仓库里常见)
PRD_DIR_CANDIDATES = [
os.path.join(REPO_ROOT, "Docs", "开发PRD"),
os.path.join(REPO_ROOT, "Docs", "PRD"),
]
TEMPLATE_PATH_DEFAULT = os.path.join(BASE_DIR, "用例文件", "兰州中石化项目测试用例20251203.xlsx")
OUTPUT_JSON_DIR = os.path.join(BASE_DIR, "config")
OUTPUT_XLSX_DIR = os.path.join(BASE_DIR, "用例文件")
headers_order = [
"序号", "功能模块", "功能类别", "用例编号", "功能描述", "用例等级",
"功能编号", "用例名称", "预置条件", "操作步骤", "JSON", "预期结果",
"测试结果", "测试结论", "日志截屏", "备注",
]
def resolve_prd_dir() -> str:
for d in PRD_DIR_CANDIDATES:
if os.path.isdir(d):
return d
# 默认返回第一个(后续会报错提示)
return PRD_DIR_CANDIDATES[0]
# ===== 2) PRD 抽取与用例生成(轻量规则)=====
@dataclass
class RequirementItem:
code: str
title: str
detail_lines: List[str]
RE_SECTION = re.compile(r"^\s*(?P<code>\d+(?:\.\d+)*)\s*[、\.\-]\s*(?P<title>.+?)\s*[::]?\s*$")
RE_MD_HEADING = re.compile(r"^\s*(#{1,6})\s+(?P<title>.+?)\s*$")
def normalize_md(text: str) -> str:
text = text.replace("\r\n", "\n").replace("\r", "\n")
text = re.sub(r"```.*?```", "", text, flags=re.S)
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
return text
def clean_line(line: str) -> str:
line = line.strip()
line = re.sub(r"^[\-\*\+]\s+", "", line)
line = re.sub(r"\s+", " ", line)
return line
def extract_requirement_items(md_text: str) -> List[RequirementItem]:
lines = md_text.split("\n")
items: List[RequirementItem] = []
current_code = ""
current_title = ""
current_detail: List[str] = []
def flush():
nonlocal current_code, current_title, current_detail
if current_title:
detail = [clean_line(x) for x in current_detail if clean_line(x)]
items.append(RequirementItem(code=current_code, title=clean_line(current_title), detail_lines=detail))
current_code = ""
current_title = ""
current_detail = []
for raw in lines:
line = raw.rstrip("\n")
m = RE_SECTION.match(line)
if m:
flush()
current_code = m.group("code").strip()
current_title = m.group("title").strip()
continue
mh = RE_MD_HEADING.match(line)
if mh:
if current_title:
current_detail.append(line)
else:
flush()
current_code = ""
current_title = mh.group("title").strip()
continue
if current_title:
current_detail.append(line)
flush()
items = [it for it in items if it.title and len(it.title) >= 2]
return items
def classify_category(title: str, detail_lines: List[str]) -> str:
text = (title + " " + " ".join(detail_lines)).lower()
if any(k in text for k in ["异常", "失败", "错误", "告警", "报警", "超时", "断开", "暴涨"]):
return "异常场景"
if any(k in text for k in ["安全", "权限", "鉴权", "加密", "脱敏"]):
return "安全/鉴权"
if any(k in text for k in ["报告", "输出", "word", "markdown", "邮件", "钉钉", "通知"]):
return "运维可观测"
return "功能测试"
def decide_priority(title: str, detail_lines: List[str]) -> str:
text = title + " " + " ".join(detail_lines)
if any(k in text for k in ["必须", "报警", "告警", "峰值", "暴涨", "发送", "对接"]):
return "高"
if any(k in text for k in ["待实现", "可以", "建议", "优化"]):
return "中"
return "中"
def build_steps(detail_lines: List[str]) -> str:
candidates: List[str] = []
for ln in detail_lines:
s = clean_line(ln)
if not s:
continue
if any(k in s for k in ["检查", "监测", "记录", "输出", "发送", "查询", "进入", "生成", "判定", "调用", "执行"]):
candidates.append(s)
uniq: List[str] = []
for c in candidates:
if c not in uniq:
uniq.append(c)
if not uniq:
return "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果"
uniq = uniq[:6]
return "; ".join([f"{i+1}. {x}" for i, x in enumerate(uniq)])
def build_expected(title: str, detail_lines: List[str]) -> str:
candidates: List[str] = []
for ln in detail_lines:
s = clean_line(ln)
if not s:
continue
if any(k in s for k in ["需要", "应", "必须", "输出", "记录", "发送", "判定", "成功", "失败"]):
candidates.append(s)
uniq: List[str] = []
for c in candidates:
if c not in uniq:
uniq.append(c)
if not uniq:
return f"{title} 按 PRD 约定产出正确结果(请补充具体断言点)"
uniq = uniq[:6]
return "; ".join(uniq)
def make_case_id(prefix: str, idx: int) -> str:
return f"{prefix}-{idx:03d}"
def to_case_record(idx: int, module: str, prefix: str, req: RequirementItem) -> Dict[str, str]:
record: Dict[str, str] = {
"序号": idx,
"功能模块": module,
"功能类别": classify_category(req.title, req.detail_lines),
"用例编号": make_case_id(prefix, idx),
"功能描述": req.title,
"用例等级": decide_priority(req.title, req.detail_lines),
"功能编号": req.code or "",
"用例名称": req.title,
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": build_steps(req.detail_lines),
"JSON": "",
"预期结果": build_expected(req.title, req.detail_lines),
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": "",
}
for k in headers_order:
record.setdefault(k, "")
return record
def prd_to_cases(prd_path: str, module: str, prefix: str, start_idx: int = 1) -> List[Dict[str, str]]:
with open(prd_path, "r", encoding="utf-8") as f:
md = normalize_md(f.read())
items = extract_requirement_items(md)
cases: List[Dict[str, str]] = []
idx = start_idx
for it in items:
if any(k in it.title for k in ["说明", "目录", "背景", "概述"]):
continue
cases.append(to_case_record(idx=idx, module=module, prefix=prefix, req=it))
idx += 1
return cases
# ===== 3) Excel 写入 =====
def find_header_row(template_sheet) -> int:
must_keys = {"序号", "用例名称", "操作步骤", "预期结果"}
max_scan = min(30, template_sheet.max_row or 30)
for r in range(1, max_scan + 1):
values = []
for c in range(1, min(40, template_sheet.max_column or 40) + 1):
v = template_sheet.cell(row=r, column=c).value
if v is None:
continue
values.append(str(v).strip())
if len(must_keys.intersection(values)) >= 2:
return r
return 3
def safe_sheet_name(name: str) -> str:
name = re.sub(r"[\[\]\:\*\?\/\\]", "_", name)
return name[:31]
def write_cases_to_sheet(wb, template_sheet, sheet_name: str, cases: List[Dict[str, str]]):
# 多选时需求:写入同一个 sheet;如果存在则复用并追加,否则创建
if sheet_name in wb.sheetnames:
ws = wb[sheet_name]
start_row = (ws.max_row or 1) + 1
else:
ws = wb.create_sheet(sheet_name)
start_row = 2
header_row_index = find_header_row(template_sheet)
for col_idx, cell in enumerate(template_sheet[header_row_index], start=1):
new_cell = ws.cell(row=1, column=col_idx, value=cell.value)
if cell.has_style:
new_cell.font = copy(cell.font)
new_cell.fill = copy(cell.fill)
new_cell.border = copy(cell.border)
new_cell.alignment = copy(cell.alignment)
new_cell.number_format = cell.number_format
ws.freeze_panes = "B2"
# 写入/追加数据
row = start_row
for case in cases:
for col_idx, header in enumerate(headers_order, start=1):
val = case.get(header, "")
if header in ("预置条件", "操作步骤") and isinstance(val, str):
val = val.replace("; ", "\n")
if header == "JSON":
val = ""
ws.cell(row=row, column=col_idx, value=val)
row += 1
# 自动换行
col_idx_pre = headers_order.index("预置条件") + 1
col_idx_steps = headers_order.index("操作步骤") + 1
for r in range(1, row):
ws.cell(row=r, column=col_idx_pre).alignment = Alignment(wrap_text=True, vertical="top")
ws.cell(row=r, column=col_idx_steps).alignment = Alignment(wrap_text=True, vertical="top")
# 列宽(仅在首次创建时做一次也可,这里简化:每次都做一次)
for col in ws.columns:
max_len = 0
col_letter = col[0].column_letter
for c in col:
v = c.value
if v is None:
continue
l = len(str(v))
if l > max_len:
max_len = l
ws.column_dimensions[col_letter].width = min(max_len + 2, 60)
return ws
# ===== 4) 多选交互 =====
def list_prd_files(prd_dir: str) -> List[str]:
if not os.path.isdir(prd_dir):
return []
fs = [f for f in os.listdir(prd_dir) if f.lower().endswith(".md")]
fs.sort()
return fs
def parse_multi_input(s: str) -> List[str]:
s = (s or "").strip()
if not s:
return []
parts = re.split(r"[,\s]+", s)
return [p for p in (x.strip() for x in parts) if p]
def pick_prds_interactively(prd_dir: str) -> List[str]:
files = list_prd_files(prd_dir)
if not files:
raise RuntimeError(f"PRD目录为空或不存在:{prd_dir}")
print(f"PRD目录:{prd_dir}")
for i, f in enumerate(files, start=1):
print(f"{i:>2}. {f}")
print("支持多选:输入多个序号(如 1,2,5 或 1 2 5)。")
while True:
s = input("请输入PRD序号(可多选):").strip()
parts = parse_multi_input(s)
if not parts or not all(p.isdigit() for p in parts):
print("输入无效,请输入序号(可多选)。")
continue
idxs: List[int] = []
ok = True
for p in parts:
n = int(p)
if not (1 <= n <= len(files)):
ok = False
break
idxs.append(n)
if not ok:
print("序号超出范围,请重试。")
continue
seen = set()
paths: List[str] = []
for n in idxs:
if n in seen:
continue
seen.add(n)
paths.append(os.path.join(prd_dir, files[n - 1]))
return paths
def ask_sheet_name_interactively(default_name: str) -> str:
s = input(f"请输入Sheet名称(回车使用默认:{default_name}):").strip()
return safe_sheet_name(s or default_name)
def ask_output_name_interactively(default_name: str) -> str:
s = input(f"请输入生成的测试用例文件名称(回车使用默认:{default_name}):").strip()
return s or default_name
def main():
parser = argparse.ArgumentParser(description="根据PRD生成用例JSON,并写入测试用例Excel")
parser.add_argument("--template", default=TEMPLATE_PATH_DEFAULT, help="模板Excel路径")
parser.add_argument("--module", default="通用模块", help="功能模块字段值(多PRD时统一使用该值)")
parser.add_argument("--prefix", default="TC", help="用例编号前缀(多PRD时统一使用该值)")
parser.add_argument("--overwrite", action="store_true", help="是否覆盖保存到模板文件(默认另存为新文件)")
args = parser.parse_args()
prd_dir = resolve_prd_dir()
if not os.path.exists(args.template):
print("找不到模板文件:", args.template)
return
# ✅ 需求:交互选择 PRD(可多选)
prd_paths = pick_prds_interactively(prd_dir)
# ✅ 需求:多选时生成在同一个 sheet;sheet 名 = 交互输入的“测试用例名称”
if len(prd_paths) == 1:
default_sheet = f"{os.path.splitext(os.path.basename(prd_paths[0]))[0]}_用例"
else:
default_sheet = "多PRD_用例"
sheet_name = ask_sheet_name_interactively(default_sheet)
# ✅ 需求:输出文件名 = 交互输入名称 + 时间戳
if len(prd_paths) == 1:
default_out_base = os.path.splitext(os.path.basename(prd_paths[0]))[0]
else:
default_out_base = "MultiPRD"
out_base = ask_output_name_interactively(default_out_base)
# 打开模板
wb = load_workbook(args.template)
template_sheet = wb.worksheets[0]
# 多 PRD 合并到同一个 sheet:序号递增,避免重复
os.makedirs(OUTPUT_JSON_DIR, exist_ok=True)
total_cases = 0
next_idx = 1
for prd_path in prd_paths:
prd_base = os.path.splitext(os.path.basename(prd_path))[0]
cases = prd_to_cases(prd_path=prd_path, module=args.module, prefix=args.prefix, start_idx=next_idx)
if not cases:
print(f"未从PRD抽取到可生成用例的条目:{prd_path}")
continue
# 每个 PRD 仍然单独落 JSON(便于追溯)
json_path = os.path.join(OUTPUT_JSON_DIR, f"{prd_base}_用例.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(cases, f, ensure_ascii=False, indent=4)
print(f"已生成 JSON:{json_path}({len(cases)}条)")
# 写入同一个 sheet(追加)
write_cases_to_sheet(wb, template_sheet, sheet_name, cases)
total_cases += len(cases)
next_idx += len(cases)
# 保存 Excel
if args.overwrite:
out_xlsx = args.template
else:
os.makedirs(OUTPUT_XLSX_DIR, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_xlsx = os.path.join(OUTPUT_XLSX_DIR, f"{out_base}_{ts}.xlsx")
wb.save(out_xlsx)
print("已生成Excel:", out_xlsx)
print("Sheet:", sheet_name)
print("总用例条数:", total_cases)
if __name__ == "__main__":
main()
\ No newline at end of file
......@@ -31,6 +31,24 @@
- 日志/截图/照片:用于记录测试过程中产生的日志、截图、照片,用于测试结果分析。
- 备注:用于记录测试过程中产生的问题,用于问题排查。
#### 用例设计
- 功能性设计方案
- 等价类划分:将输入数据划分为有效/无效等价类,减少冗余用例。
- 示例:输入框限制1-100的整数,有效等价类为“1≤x≤100”,无效类包括“x<1”“x>100”“非整数”。
- 边界值分析:针对输入范围的边界设计用例(如边界±1)。
- 示例:测试年龄输入框,需覆盖0、1、100、101等值。
- 因果图与判定表:分析输入条件与输出结果的逻辑关系,适用于复杂业务规则。
- 示例:订单状态变更涉及“支付成功”“库存充足”“物流就绪”等多个条件组合。
- 场景法:模拟用户实际操作流程(如登录→浏览商品→下单→支付)。
- 错误推测法:基于经验预判易错点(如空指针、并发冲突)。
- 非功能性测试
- 性能测试:模拟高并发场景(如秒杀活动),监控响应时间、吞吐量。
- 兼容性测试:覆盖主流浏览器(Chrome/Firefox)、设备分辨率及操作系统。
- 安全测试:SQL注入、XSS攻击防护、敏感数据加密。
- 环境差异测试:测试不同浏览器、设备分辨率及操作系统下的兼容性。
- 遗漏场景:通过探索性测试补充用例未覆盖的边缘路径。
#### 功能实现方式
- AI服务:通过AI服务调用,对需求文档和开发需求PRD文档进行解析和理解,提取功能点,并生成测试用例内容。
- API模式:通过兼容Anthropic API进行调用:
......@@ -40,6 +58,7 @@
- CLT模式:通过本地claude命令行工具调用,使用现有session token
- 安装: npm install -g @anthropic/claude
- Auto模式:自动检测,优先使用API模式,若API模式不可用则使用CLT模式。
- 日志打印:将发送给AI的提示词打印到控制台中,方便后续查看。
- 需求认知:阅读需求文档中功能描述,确定功能需求。
- 获取测试用例:从测试用例文件中获取当前已有的测试用例内容。
......
......@@ -96,6 +96,42 @@ response = client.messages.create(
自动检测,优先使用API模式,若API模式不可用则使用CLI模式:
- 配置:`AI_SERVICE_MODE=auto`
#### 日志打印
为了方便调试和查看AI服务调用情况,工具会自动将发送给AI的提示词打印到控制台:
**打印格式:**
```
================================================================================
[AI服务调用] 模型: claude-sonnet-4-6 | 方法: ask_json | 模式: api
================================================================================
[完整提示词]
你是一个专业的测试用例分析专家...
(提示词完整内容)
================================================================================
```
**对于长提示词(>2000字符):**
```
================================================================================
[AI服务调用] 模型: claude-sonnet-4-6 | 方法: ask_json_array | 模式: api
================================================================================
[提示词摘要] 总长度: 5432 字符
--------------------------------------------------------------------------------
[前500字符]
你是一个专业的测试用例分析专家...
(前500字符内容)
... (省略中间内容) ...
[后500字符] (从 4932 开始)
(后500字符内容)
================================================================================
```
**日志记录:**
- 同时记录到日志文件:`logs/ai_test_case_enhancer.log`
- 日志格式:`[AI服务调用] 模型=xxx, 方法=xxx, 模式=xxx, 提示词长度=xxx`
### 3.3 核心流程
```
......@@ -392,11 +428,11 @@ class TestCaseAnalyzer:
缺失检测模块
"""
from typing import List, Dict
import json
from typing import List, Dict, Optional
from logging import getLogger
import anthropic
from src.config import AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE
from src.config import AI_MODEL, COVERAGE_TARGET, TEST_DESIGN_METHODS
logger = getLogger(__name__)
......@@ -404,104 +440,55 @@ logger = getLogger(__name__)
class GapDetector:
"""缺失检测器"""
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
def __init__(self, api_key: Optional[str] = None, use_cli: bool = False):
"""
初始化检测器
支持API模式和CLI模式
"""
from src.ai_service_manager import AIServiceManager
self.ai_service = AIServiceManager(api_key=api_key, use_cli=use_cli)
def detect_gaps(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""
检测测试用例缺失情况
Returns:
缺失检测结果
"""
"""检测测试用例缺失情况"""
logger.info("开始检测测试用例缺失...")
# 使用AI分析缺失情况
gaps = self._ai_detect_gaps(function_points, test_cases, requirements, prds)
return {
"功能点缺失": gaps.get("功能点缺失", []),
"场景覆盖不足": gaps.get("场景覆盖不足", []),
"边界条件不足": gaps.get("边界条件不足", [])
}
def _ai_detect_gaps(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""使用AI检测缺失"""
prompt = self._build_gap_prompt(function_points, test_cases, requirements, prds)
response = self.client.messages.create(
model=AI_MODEL,
max_tokens=AI_MAX_TOKENS,
temperature=AI_TEMPERATURE,
messages=[{"role": "user", "content": prompt}]
)
result = response.content[0].text
return self._parse_gap_result(result)
return gaps
def _build_gap_prompt(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> str:
"""构建缺失检测提示词"""
prompt = """请分析以下功能点、需求文档、PRD文档和现有测试用例,识别测试用例的缺失情况。
"""构建缺失检测提示词,集成测试设计方法"""
prompt = """你是一个专业的测试用例分析专家。请分析以下功能点、需求文档、PRD文档和现有测试用例,识别测试用例的缺失情况。
【功能点列表】
"""
prompt += str(function_points)
prompt += json.dumps(function_points, ensure_ascii=False, indent=2)
# 添加现有测试用例和需求文档摘要...
prompt += f"\n\n【现有测试用例】(共{len(test_cases)}条)\n"
for i, case in enumerate(test_cases[:20]): # 限制数量
prompt += f"{i+1}. {case.get('用例名称', '')} - {case.get('功能描述', '')}\n"
prompt += "\n\n请识别以下三类缺失,输出JSON格式:{...}"
# 集成测试设计方法
prompt += "\n\n" + TEST_DESIGN_METHODS
prompt += "\n请识别以下三类缺失,输出JSON格式:"
prompt += """
{
"功能点缺失": [
{
"功能模块": "模块名",
"功能编号": "F-001",
"功能描述": "缺失的功能描述",
"建议用例名称": "测试XXX功能"
}
],
"场景覆盖不足": [
{
"功能描述": "XXX功能",
"缺失场景": "异常场景:XXX",
"建议用例名称": "测试XXX异常场景"
}
],
"边界条件不足": [
{
"功能描述": "XXX功能",
"缺失边界": "边界值:XXX",
"建议用例名称": "测试XXX边界条件"
}
]
}
【额外要求】
1. 应用上述测试设计方法进行缺失分析
2. 特别关注等价类划分、边界值分析、场景法等设计方法的应用
3. 识别性能测试、安全测试、兼容性测试等非功能性测试场景
"""
return prompt
def _parse_gap_result(self, result: str) -> Dict:
"""解析AI返回的缺失结果"""
import json
try:
start = result.find("{")
end = result.rfind("}") + 1
json_str = result[start:end]
return json.loads(json_str)
except Exception as e:
logger.error("解析缺失结果失败: %s", e)
return {"功能点缺失": [], "场景覆盖不足": [], "边界条件不足": []}
```
**关键更新:**
- 集成`TEST_DESIGN_METHODS`常量到AI提示词中
- 在缺失检测时应用测试设计方法
- 增加设计方法和场景类型的识别
### 4.5 用例生成模块 (case_generator.py)
```python
......@@ -510,11 +497,15 @@ class GapDetector:
用例生成模块
"""
from typing import List, Dict
from typing import List, Dict, Optional
from logging import getLogger
import anthropic
import json
from src.config import AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE, TEST_CASE_COLUMNS
from src.config import (
AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE,
TEST_CASE_COLUMNS, CASE_LEVELS, DEFAULT_CASE_LEVEL,
DEFAULT_TEST_FREQUENCY, TEST_DESIGN_METHODS
)
logger = getLogger(__name__)
......@@ -522,97 +513,177 @@ logger = getLogger(__name__)
class CaseGenerator:
"""测试用例生成器"""
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
def __init__(self, api_key: Optional[str] = None, use_cli: bool = False):
"""初始化生成器,支持API和CLI模式"""
from src.ai_service_manager import AIServiceManager
self.ai_service = AIServiceManager(api_key=api_key, use_cli=use_cli)
self.case_counter = 1
def generate_cases(self, gaps: Dict, requirements: List[Dict],
prds: List[Dict]) -> List[Dict]:
"""
生成缺失的测试用例
Returns:
新生成的测试用例列表
"""
prds: List[Dict], existing_cases: List[Dict]) -> List[Dict]:
"""生成缺失的测试用例"""
logger.info("开始生成测试用例...")
new_cases = []
# 获取下一个序列号
next_seq = self._get_next_sequence(existing_cases)
# 为功能点缺失生成用例
for gap in gaps.get("功能点缺失", []):
case = self._generate_case_for_function(gap, requirements, prds)
if case:
new_cases.append(case)
function_cases = self._generate_cases_for_functions(
gaps.get("功能点缺失", []),
requirements, prds, next_seq
)
new_cases.extend(function_cases)
# 为场景覆盖不足生成用例
for gap in gaps.get("场景覆盖不足", []):
case = self._generate_case_for_scenario(gap, requirements, prds)
if case:
new_cases.append(case)
scenario_cases = self._generate_cases_for_scenarios(
gaps.get("场景覆盖不足", []),
requirements, prds, next_seq + len(function_cases)
)
new_cases.extend(scenario_cases)
# 为边界条件不足生成用例
for gap in gaps.get("边界条件不足", []):
case = self._generate_case_for_boundary(gap, requirements, prds)
if case:
new_cases.append(case)
boundary_cases = self._generate_cases_for_boundaries(
gaps.get("边界条件不足", []),
requirements, prds, next_seq + len(function_cases) + len(scenario_cases)
)
new_cases.extend(boundary_cases)
logger.info("生成 %d 条新测试用例", len(new_cases))
return new_cases
def _generate_case_for_function(self, gap: Dict, requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""为功能点缺失生成测试用例"""
prds: List[Dict], seq: int) -> Optional[Dict]:
"""为功能点缺失生成测试用例,集成测试设计方法"""
prompt = f"""请为以下缺失的功能点生成完整的测试用例。
【功能信息】
- 功能模块:{gap.get('功能模块')}
- 功能编号:{gap.get('功能编号')}
- 功能描述:{gap.get('功能描述')}
请生成测试用例,输出JSON格式(包含以下字段):
{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
要求:
1. 用例编号格式:TC-模块-序号
2. 用例等级:P1/P2/P3
3. STEP要详细,包含操作步骤
4. 预期结果要明确
- 功能模块:{gap.get('功能模块', '')}
- 功能编号:{gap.get('功能编号', '')}
- 功能描述:{gap.get('功能描述', '')}
- 建议设计方法:{gap.get('设计方法', '场景法')}
【要求】
1. 应用等价类划分和边界值分析方法
2. 生成详细的测试步骤
3. 预期结果要明确具体
请输出JSON格式的测试用例:{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
"""
response = self.client.messages.create(
model=AI_MODEL,
max_tokens=AI_MAX_TOKENS,
temperature=AI_TEMPERATURE,
messages=[{"role": "user", "content": prompt}]
)
# 集成测试设计方法
prompt += f"\n\n{TEST_DESIGN_METHODS}"
result = response.content[0].text
return self._parse_case(result)
prompt += f"""
【额外要求】
1. 用例编号格式:TC-{gap.get('功能模块', 'TEST')[:4].upper()}-{seq:03d}
2. 用例等级:{gap.get('优先级', DEFAULT_CASE_LEVEL)}
3. 根据功能特点选择合适的测试设计方法
4. 对于输入框类功能,必须包含边界值测试
5. 对于权限类功能,必须包含不同角色的权限验证
"""
return self._call_ai_for_case(prompt, gap.get("功能描述", ""))
def _generate_case_for_scenario(self, gap: Dict, requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""为场景覆盖不足生成测试用例"""
# 类似实现
pass
prds: List[Dict], seq: int) -> Optional[Dict]:
"""为场景覆盖不足生成测试用例,应用场景法"""
prompt = f"""请为以下缺失的场景生成完整的测试用例。
【功能信息】
- 功能描述:{gap.get('功能描述', '')}
- 已有场景:{gap.get('已有场景', '')}
- 缺失场景:{gap.get('缺失场景', '')}
- 场景类型:{gap.get('场景类型', '异常场景')}
请输出JSON格式的测试用例:{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
"""
# 集成测试设计方法
prompt += f"\n\n{TEST_DESIGN_METHODS}"
prompt += """
【场景设计指导】
根据场景类型应用相应的设计方法:
1. 异常场景:输入无效数据、模拟操作失败
2. 边界场景:测试边界值、空值和null值
3. 并发场景:模拟多用户同时操作
4. 权限场景:不同角色权限验证、越权访问测试
"""
return self._call_ai_for_case(prompt, gap.get("缺失场景", ""))
def _generate_case_for_boundary(self, gap: Dict, requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""为边界条件不足生成测试用例"""
# 类似实现
pass
prds: List[Dict], seq: int) -> Optional[Dict]:
"""为边界条件不足生成测试用例,应用边界值分析方法"""
prompt = f"""请为以下缺失的边界条件生成完整的测试用例。
def _parse_case(self, result: str) -> Dict:
"""解析AI返回的测试用例"""
import json
try:
start = result.find("{")
end = result.rfind("}") + 1
json_str = result[start:end]
return json.loads(json_str)
except Exception as e:
logger.error("解析测试用例失败: %s", e)
return {}
【功能信息】
- 功能描述:{gap.get('功能描述', '')}
- 缺失边界:{gap.get('缺失边界', '')}
- 边界类型:{gap.get('边界类型', '边界值')}
请输出JSON格式的测试用例:{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
"""
# 集成测试设计方法
prompt += f"\n\n{TEST_DESIGN_METHODS}"
prompt += """
【边界值设计指导】
1. 数值型边界:上边界、上边界+1、上边界-1、下边界、下边界+1、下边界-1、零值
2. 字符串边界:空字符串、最大长度、超长字符串、特殊字符
3. 列表/集合边界:空列表、单元素、最大数量、超过最大值
4. 日期时间边界:最小日期、最大日期、边界日期±1天、无效日期
"""
return self._call_ai_for_case(prompt, gap.get("缺失边界", ""))
```
**关键更新:**
- 集成`TEST_DESIGN_METHODS`常量到所有生成方法的AI提示词中
- 根据缺失类型(功能点、场景、边界)应用不同的设计方法指导
- 在提示词中明确要求使用等价类划分、边界值分析、场景法等方法
- 针对不同场景类型(异常、边界、并发、权限)提供具体设计指导
### 4.6 配置模块更新 (config.py)
在配置模块中新增测试用例设计方法常量:
```python
# ==================== 测试用例设计方法 ====================
# 测试设计方法提示词模板
TEST_DESIGN_METHODS = """
在生成测试用例时,请应用以下测试设计方法:
【功能性测试设计方法】
1. 等价类划分:识别有效等价类和无效等价类
- 有效等价类:符合需求规格说明的输入数据
- 无效等价类:空值、特殊字符、超长字符串、非法格式等
2. 边界值分析:测试边界值及边界±1的值
- 上边界、上边界+1、上边界-1
- 下边界、下边界+1、下边界-1
3. 场景法:设计完整的用户操作流程
- 基本流:正常操作流程
- 备选流:异常处理流程
4. 错误推测法:基于经验设计缺陷探测用例
- 空指针、并发冲突、数据溢出
- SQL注入、XSS攻击、权限越界
【非功能性测试设计】
1. 性能测试:大数据量、高并发场景
2. 兼容性测试:不同浏览器、设备、操作系统
3. 安全测试:SQL注入、XSS攻击、越权访问
4. 异常场景:网络中断、服务不可用
【测试用例优先级判定标准】
- P0(最高):核心功能、关键业务路径、安全相关
- P1(高):重要功能、常用场景、性能相关
- P2(中):辅助功能、一般场景、兼容性相关
- P3(低):边缘功能、异常场景、UI美化相关
"""
```
### 4.6 去重模块 (deduplicator.py)
### 4.7 去重模块 (deduplicator.py)
```python
# -*- coding: utf-8 -*-
......@@ -932,9 +1003,151 @@ if __name__ == "__main__":
---
## 5. 实施计划
## 5. 测试用例设计方案
### 5.1 功能性设计方案
#### 5.1.1 等价类划分
将输入数据划分为有效/无效等价类,减少冗余用例。
**设计原则:**
- 有效等价类:符合需求规格说明的输入数据
- 无效等价类:违反需求规格说明的输入数据
- 每个等价类只需选取一个代表值进行测试
**示例:**
- 输入框限制1-100的整数
- 有效等价类:`1≤x≤100`
- 无效等价类:`x<1``x>100``非整数``空值`
#### 5.1.2 边界值分析
针对输入范围的边界设计用例,因为错误常发生在边界处。
**设计原则:**
- 上边界、上边界+1、上边界-1
- 下边界、下边界+1、下边界-1
**示例:**
- 测试年龄输入框(1-100岁)
- 测试值:`0``1``2``99``100``101`
#### 5.1.3 因果图与判定表
分析输入条件与输出结果的逻辑关系,适用于复杂业务规则。
**设计步骤:**
1. 列出输入条件(原因)和输出结果(结果)
2. 分析因果关系,绘制因果图
3. 将因果图转换为判定表
4. 根据判定表设计测试用例
**示例:**
- 订单状态变更涉及多个条件组合:
- 条件:支付成功、库存充足、物流就绪
- 结果:订单确认、订单取消、订单挂起
#### 5.1.4 场景法
模拟用户实际操作流程,覆盖完整的业务流程。
**设计原则:**
- 基本流:正常操作流程
- 备选流:异常处理流程
- 按照用户实际使用场景设计
**示例:**
- 用户登录流程:
- 基本流:输入正确账号密码 → 登录成功
- 备选流:输入错误密码 → 提示重试
- 备选流:账号不存在 → 提示注册
#### 5.1.5 错误推测法
基于经验预判易错点,主动设计缺陷探测用例。
**设计方向:**
- 空指针场景
- 并发冲突
- 数据溢出
- 特殊字符处理
- 网络异常
- 权限越界
### 5.2 非功能性测试设计
#### 5.2.1 性能测试
**测试目标:** 验证系统在特定条件下的响应时间和吞吐量
**设计方法:**
- 响应时间测试:测量单个操作的响应时间
- 并发测试:模拟多用户同时操作
- 压力测试:测试系统极限承载能力
- 示例场景:秒杀活动、报表导出
#### 5.2.2 兼容性测试
**测试目标:** 验证系统在不同环境下的正常运行
**覆盖范围:**
- **浏览器:** Chrome、Firefox、Edge、Safari
- **设备分辨率:** 1920x1080、1366x768、移动端适配
- **操作系统:** Windows、macOS、Linux
#### 5.2.3 安全测试
**测试目标:** 发现系统的安全漏洞
**测试类型:**
- **SQL注入:** 输入特殊SQL语句尝试注入
- **XSS攻击:** 输入脚本代码测试防护
- **敏感数据:** 检查密码、token等是否加密存储
- **权限控制:** 验证越权访问是否被阻止
#### 5.2.4 环境差异测试
**测试目标:** 确保系统在不同环境下的兼容性
**测试维度:**
- 浏览器版本差异
- 设备分辨率差异
- 操作系统差异
- 网络环境差异(4G/5G/WiFi)
#### 5.2.5 遗漏场景补充
**测试目标:** 通过探索性测试补充用例未覆盖的边缘路径
**补充方法:**
- 回顾历史bug,补充回归测试用例
- 分析用户反馈,补充用户体验场景
- 探索性测试,发现未预料的场景
### 5.3 测试用例设计提示词模板
基于以上设计方案,在AI生成测试用例时应遵循以下提示词模板:
```python
TEST_DESIGN_METHODS = """
在生成测试用例时,请应用以下测试设计方法:
【功能性测试设计方法】
1. 等价类划分:识别有效等价类和无效等价类,为每个等价类设计代表性用例
2. 边界值分析:针对有范围限制的输入,测试边界值及边界±1的值
3. 场景法:设计完整的用户操作流程,包括基本流和备选流
4. 错误推测:基于经验设计特殊字符、空值、异常输入等测试场景
【非功能性测试设计】
1. 性能测试:设计大数据量、高并发场景的测试用例
2. 兼容性测试:考虑不同浏览器、设备的兼容性
3. 安全测试:设计SQL注入、XSS攻击、越权访问等安全测试用例
4. 异常场景:设计网络中断、服务不可用等异常情况
【测试用例优先级】
- P0:核心功能、关键路径
- P1:重要功能、常用场景
- P2:辅助功能、一般场景
- P3:边缘功能、异常场景
"""
```
---
## 6. 实施计划
### 5.1 实施步骤
### 6.1 实施步骤
| 序号 | 任务 | 预计时间 | 状态 |
|------|------|----------|------|
......@@ -967,7 +1180,7 @@ pandas>=2.0.0
---
## 6. 测试验证
## 7. 测试验证
### 6.1 单元测试
......@@ -995,7 +1208,7 @@ pandas>=2.0.0
---
## 7. 注意事项
## 8. 注意事项
1. **API密钥安全**:ANTHROPIC_API_KEY 应通过环境变量设置,不要硬编码在代码中
2. **提示词优化**:实际使用中可能需要根据AI返回结果优化提示词
......@@ -1004,7 +1217,7 @@ pandas>=2.0.0
---
## 8. 后续优化
## 9. 后续优化
- [ ] 支持自定义AI提示词模板
- [ ] 支持增量更新(只处理变更的部分)
......@@ -1012,3 +1225,6 @@ pandas>=2.0.0
- [ ] 支持导出为多种格式
- [ ] 添加用例质量评分
- [ ] 支持批量处理多个项目
- [ ] 支持等价类划分、边界值分析等设计方法的自动化应用
- [ ] 支持测试用例优先级的自动判定
- [ ] 支持性能测试用例、安全测试用例的分类生成
# AI完善用例需求文档
## 代码路径
- 代码路径:[AuxiliaryTool/AIPerfectedTestCases]
## 功能需求
### 功能目标
**目标:** 当前AI提示词过长(3000+字符),存在覆盖度下降、解析失败、生成数量少的潜在问题,需要优化处理逻辑。
**问题分析:**
- AI注意力分散:长提示词导致AI忽略关键指令
- 信息过载:核心任务被淹没
- 指令冲突:多个设计方法要求互相干扰
### 需求描述
#### 文档路径获取
- 文档路径:[AuxiliaryTool/AIPerfectedTestCases/config]
- 需求文档:`config/需求文档/*.docx`
- 开发需求PRD文档:`config/开发PRD/*.md`
- 测试用例:`config/测试用例/新统一平台权限管理测试用例.xlsx`
#### 方案选择:A+B组合方案
采用**分步调用(方案A)+ 精简提示词(方案B)**的组合:
- 使用A方案的3步调用,将复杂任务拆分
- 每步内部使用B方案的精简提示词结构
- 每个提示词控制在1000字符以内
#### 步骤1:提取功能点
**目标:** 从需求文档中提取功能点列表
**提示词长度:** ~500字符
**输入:**
- 需求文档内容(限制300字符)
- PRD文档内容(限制300字符)
**输出格式:**
```json
[
{"功能模块": "用户管理", "功能描述": "用户登录功能"},
{"功能模块": "权限管理", "功能描述": "角色权限分配"}
]
```
**提示词模板:**
```python
def _build_step1_prompt(self, requirements: List[Dict], prds: List[Dict]) -> str:
"""构建步骤1提示词:提取功能点"""
# 格式化需求文档,限制长度
req_text = self._format_requirements(requirements, prds, max_chars=300)
prompt = f"""你是一个测试专家。分析需求文档,提取功能点列表。
【需求文档】
{req_text}
【要求】
1. 识别所有功能模块
2. 每个功能包含:功能模块、功能描述
3. 输出JSON数组格式
【输出格式】
[{{"功能模块": "模块名", "功能描述": "功能描述"}}]
"""
return prompt
```
#### 步骤2:检测缺失
**目标:** 对比功能点和现有用例,识别三类缺失
**提示词长度:** ~800字符
**输入:**
- 步骤1提取的功能点
- 现有测试用例摘要(前20条,只包含用例名称和功能描述)
**输出格式:**
```json
{
"功能点缺失": [
{"功能模块": "模块名", "功能描述": "缺失功能", "建议用例名称": "测试xxx", "优先级": "P1"}
],
"场景覆盖不足": [
{"功能描述": "xxx功能", "已有场景": "正常场景", "缺失场景": "异常场景", "建议用例名称": "测试xxx异常", "优先级": "P2"}
],
"边界条件不足": [
{"功能描述": "xxx功能", "已有边界": "常规值", "缺失边界": "边界值:0", "建议用例名称": "测试xxx边界", "优先级": "P2"}
]
}
```
**提示词模板:**
```python
def _build_step2_prompt(self, function_points: List[Dict], test_cases: List[Dict]) -> str:
"""构建步骤2提示词:检测缺失"""
# 格式化测试用例摘要
cases_summary = self._summarize_test_cases(test_cases, max_items=20)
prompt = f"""你是一个测试专家。对比功能点和现有用例,找出缺失。
【测试设计方法】
等价类划分、边界值分析、场景法、错误推测法、因果图判定表、性能测试、安全测试、兼容性测试
【功能点】
{json.dumps(function_points, ensure_ascii=False)}
【现有测试用例摘要】(共{len(test_cases)}条)
{cases_summary}
【输出格式】严格遵循JSON:
{{
"功能点缺失": [{{"功能模块": "", "功能描述": "", "建议用例名称": "", "优先级": "P1"}}],
"场景覆盖不足": [{{"功能描述": "", "已有场景": "", "缺失场景": "", "建议用例名称": "", "优先级": "P2"}}],
"边界条件不足": [{{"功能描述": "", "已有边界": "", "缺失边界": "", "建议用例名称": "", "优先级": "P2"}}]
}}
【要求】
1. 优先级:P0(最高)、P1、P2、P3(最低)
2. 只输出JSON对象,不要其他文字
3. 识别缺失时应用上述测试设计方法
"""
return prompt
```
#### 步骤3:生成用例
**目标:** 为缺失的功能点、场景、边界条件生成测试用例
**提示词长度:** ~600字符
**输入:**
- 步骤2检测到的缺失信息
- 需求文档摘要(可选,200字符)
**输出格式:**
```json
{
"序列号": 1,
"功能模块": "用户管理",
"用例编号": "TC-USER-001",
"功能描述": "登录功能",
"用例等级": "P1",
"功能编号": "F-001",
"用例名称": "测试用户登录成功",
"预置条件": "用户已注册",
"STEP": "1.打开登录页面\n2.输入用户名和密码\n3.点击登录",
"JSON": "暂不涉及",
"预期结果": "登录成功,跳转到首页",
"测试结果": "",
"测试频次": "每版本",
"日志/截图/照片": "",
"备注": ""
}
```
**提示词模板:**
```python
def _build_step3_prompt(self, gap: Dict, seq: int, requirements: List[Dict] = None) -> str:
"""构建步骤3提示词:生成单个测试用例"""
gap_type = gap.get("type", "功能点缺失")
prompt = f"""你是一个测试专家。为以下缺失生成测试用例。
【缺失信息】
- 类型:{gap_type}
- 功能描述:{gap.get('功能描述', '')}
"""
# 根据缺失类型添加特定信息
if gap_type == "场景覆盖不足":
prompt += f"- 已有场景:{gap.get('已有场景', '')}\n"
prompt += f"- 缺失场景:{gap.get('缺失场景', '')}\n"
elif gap_type == "边界条件不足":
prompt += f"- 已有边界:{gap.get('已有边界', '')}\n"
prompt += f"- 缺失边界:{gap.get('缺失边界', '')}\n"
prompt += f"""
【测试设计方法】
等价类划分:有效等价类、无效等价类
边界值分析:上边界、下边界、边界±1
场景法:基本流、备选流
错误推测:空值、特殊字符、异常输入
【输出格式】严格遵循JSON:
{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
【要求】
1. 用例编号格式:TC-{gap.get('功能模块', 'TEST')[:4].upper()}-{seq:03d}
2. 用例等级:{gap.get('优先级', 'P1')}
3. STEP包含详细操作步骤,每步一行
4. JSON字段填写:暂不涉及
5. 测试频次:每版本
6. 根据缺失类型应用相应的测试设计方法
7. 边界条件测试需包含具体的边界值数据
"""
return prompt
```
#### 辅助方法
**需求文档格式化(控制长度):**
```python
def _format_requirements(self, requirements: List[Dict], prds: List[Dict], max_chars: int = 300) -> str:
"""格式化需求文档,控制长度"""
parts = []
total_chars = 0
for req in requirements:
content = f"【{req['filename']}】{req['content'][:200]}..."
parts.append(content)
total_chars += len(content)
if total_chars >= max_chars:
break
return "\n".join(parts)
```
**测试用例摘要生成:**
```python
def _summarize_test_cases(self, test_cases: List[Dict], max_items: int = 20) -> str:
"""生成测试用例摘要"""
lines = []
for i, case in enumerate(test_cases[:max_items], 1):
lines.append(f"{i}. {case.get('用例名称', '')} - {case.get('功能描述', '')}")
result = "\n".join(lines)
if len(test_cases) > max_items:
result += f"\n... (还有{len(test_cases)-max_items}条)"
return result
```
#### 核心流程
```python
def main():
# 步骤1:提取功能点
function_points = analyzer.extract_function_points(requirements, prds)
# 步骤2:检测缺失
gaps = detector.detect_gaps(function_points, test_cases)
# 步骤3:生成用例(逐个生成)
new_cases = []
for gap_type, gap_list in gaps.items():
for gap in gap_list:
case = generator.generate_case(gap, seq)
if case:
new_cases.append(case)
```
## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
# AI调用逻辑及提示词缩短功能优化_计划执行
## 1. 项目概述
### 1.1 项目背景
当前AI提示词过长(3000+字符),导致AI注意力分散、覆盖度下降、解析失败、生成数量少等问题。需要采用分步调用+精简提示词的组合方案进行优化。
### 1.2 优化目标
- 每个提示词控制在1000字符以内
- 提高AI提取关键信息的准确性
- 提高测试点提取的覆盖度
- 提高缺失用例生成的数量
### 1.3 优化方案
采用 **A+B组合方案**
- **方案A(分步调用)**:将复杂任务拆分为3个独立步骤
- **方案B(精简提示词)**:每步内部使用精简的提示词结构
---
## 2. 方案设计
### 2.1 三步调用流程
```
┌─────────────────────────────────────────────────────────────────┐
│ 优化后流程 │
├─────────────────────────────────────────────────────────────────┤
│ 步骤1:提取功能点(~500字符) │
│ → 输入:需求文档摘要(300字符) │
│ → 输出:功能点JSON数组 │
├─────────────────────────────────────────────────────────────────┤
│ 步骤2:检测缺失(~800字符) │
│ → 输入:功能点 + 测试用例摘要(前20条) │
│ → 输出:3类缺失JSON对象 │
├─────────────────────────────────────────────────────────────────┤
│ 步骤3:生成用例(~600字符/条) │
│ → 输入:单个缺失信息 │
│ → 输出:完整测试用例JSON对象 │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 提示词长度对比
| 项目 | 优化前 | 优化后 |
|------|--------|--------|
| 功能点提取提示词 | 1500字符 | 500字符 |
| 缺失检测提示词 | 2500字符 | 800字符 |
| 用例生成提示词 | 2000字符 | 600字符 |
| 单次调用总字符 | 6000字符 | 1900字符 |
---
## 3. 详细设计
### 3.1 步骤1:提取功能点
**文件:** `src/test_case_analyzer.py`
**新增方法:** `extract_function_points_v2()`
```python
def extract_function_points_v2(self, requirements: List[Dict],
prds: List[Dict]) -> List[Dict]:
"""
使用精简提示词提取功能点(步骤1)
Args:
requirements: 需求文档列表
prds: PRD文档列表
Returns:
功能点列表
"""
prompt = self._build_step1_prompt(requirements, prds)
return self.ai_service.ask_json_array(prompt)
def _build_step1_prompt(self, requirements: List[Dict], prds: List[Dict]) -> str:
"""构建步骤1提示词:提取功能点"""
# 格式化需求文档,限制长度
req_text = self._format_requirements_v2(requirements, prds, max_chars=300)
prompt = f"""你是一个测试专家。分析需求文档,提取功能点列表。
【需求文档】
{req_text}
【要求】
1. 识别所有功能模块
2. 每个功能包含:功能模块、功能描述
3. 输出JSON数组格式
【输出格式】
[{{"功能模块": "模块名", "功能描述": "功能描述"}}]
"""
logger.info("步骤1提示词长度: %d 字符", len(prompt))
return prompt
def _format_requirements_v2(self, requirements: List[Dict],
prds: List[Dict], max_chars: int = 300) -> str:
"""格式化需求文档,控制长度"""
parts = []
total_chars = 0
# 合并需求和PRD
all_docs = requirements + prds
for doc in all_docs:
content = f"【{doc['filename']}】{doc['content'][:200]}..."
parts.append(content)
total_chars += len(content)
if total_chars >= max_chars:
break
return "\n".join(parts)
```
### 3.2 步骤2:检测缺失
**文件:** `src/gap_detector.py`
**新增方法:** `detect_gaps_v2()`
```python
def detect_gaps_v2(self, function_points: List[Dict],
test_cases: List[Dict]) -> Dict:
"""
使用精简提示词检测缺失(步骤2)
Args:
function_points: 功能点列表
test_cases: 测试用例列表
Returns:
缺失检测结果
"""
prompt = self._build_step2_prompt(function_points, test_cases)
return self.ai_service.ask_json(prompt)
def _build_step2_prompt(self, function_points: List[Dict],
test_cases: List[Dict]) -> str:
"""构建步骤2提示词:检测缺失"""
# 格式化测试用例摘要
cases_summary = self._summarize_test_cases_v2(test_cases, max_items=20)
prompt = f"""你是一个测试专家。对比功能点和现有用例,找出缺失。
【测试设计方法】
等价类划分、边界值分析、场景法、错误推测法、因果图判定表、性能测试、安全测试、兼容性测试
【功能点】
{json.dumps(function_points, ensure_ascii=False)}
【现有测试用例摘要】(共{len(test_cases)}条)
{cases_summary}
【输出格式】严格遵循JSON:
{{
"功能点缺失": [{{"功能模块": "", "功能描述": "", "建议用例名称": "", "优先级": "P1"}}],
"场景覆盖不足": [{{"功能描述": "", "已有场景": "", "缺失场景": "", "建议用例名称": "", "优先级": "P2"}}],
"边界条件不足": [{{"功能描述": "", "已有边界": "", "缺失边界": "", "建议用例名称": "", "优先级": "P2"}}]
}}
【要求】
1. 优先级:P0(最高)、P1、P2、P3(最低)
2. 只输出JSON对象,不要其他文字
3. 识别缺失时应用上述测试设计方法
"""
logger.info("步骤2提示词长度: %d 字符", len(prompt))
return prompt
def _summarize_test_cases_v2(self, test_cases: List[Dict], max_items: int = 20) -> str:
"""生成测试用例摘要"""
lines = []
for i, case in enumerate(test_cases[:max_items], 1):
lines.append(f"{i}. {case.get('用例名称', '')} - {case.get('功能描述', '')}")
result = "\n".join(lines)
if len(test_cases) > max_items:
result += f"\n... (还有{len(test_cases)-max_items}条)"
return result
```
### 3.3 步骤3:生成用例
**文件:** `src/case_generator.py`
**新增方法:** `generate_cases_v2()`
```python
def generate_cases_v2(self, gaps: Dict, existing_cases: List[Dict]) -> List[Dict]:
"""
使用精简提示词逐个生成测试用例(步骤3)
Args:
gaps: 缺失检测结果
existing_cases: 现有测试用例列表
Returns:
新生成的测试用例列表
"""
logger.info("开始逐个生成测试用例...")
new_cases = []
next_seq = self._get_next_sequence(existing_cases)
# 遍历所有缺失类型
for gap_type, gap_list in gaps.items():
for gap in gap_list:
try:
# 为每个缺失生成一个测试用例
case = self._generate_case_v2(gap, gap_type, next_seq)
if case:
new_cases.append(case)
next_seq += 1
except Exception as e:
logger.error("生成测试用例失败: %s, 错误: %s",
gap.get('功能描述', ''), e)
logger.info("生成测试用例完成: %d 条", len(new_cases))
return new_cases
def _generate_case_v2(self, gap: Dict, gap_type: str, seq: int) -> Optional[Dict]:
"""使用精简提示词生成单个测试用例"""
prompt = self._build_step3_prompt(gap, gap_type, seq)
return self.ai_service.ask_json(prompt)
def _build_step3_prompt(self, gap: Dict, gap_type: str, seq: int) -> str:
"""构建步骤3提示词:生成单个测试用例"""
prompt = f"""你是一个测试专家。为以下缺失生成测试用例。
【缺失信息】
- 类型:{gap_type}
- 功能描述:{gap.get('功能描述', '')}
"""
# 根据缺失类型添加特定信息
if gap_type == "场景覆盖不足":
prompt += f"- 已有场景:{gap.get('已有场景', '')}\n"
prompt += f"- 缺失场景:{gap.get('缺失场景', '')}\n"
elif gap_type == "边界条件不足":
prompt += f"- 已有边界:{gap.get('已有边界', '')}\n"
prompt += f"- 缺失边界:{gap.get('缺失边界', '')}\n"
prompt += f"""
【测试设计方法】
等价类划分:有效等价类、无效等价类(空值、特殊字符、超长值)
边界值分析:上边界、下边界、边界±1
场景法:基本流、备选流
错误推测:异常输入、并发冲突
【输出格式】严格遵循JSON:
{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
【要求】
1. 用例编号格式:TC-{gap.get('功能模块', 'TEST')[:4].upper()}-{seq:03d}
2. 用例等级:{gap.get('优先级', 'P1')}
3. STEP包含详细操作步骤,每步一行
4. JSON字段填写:暂不涉及
5. 测试频次:每版本
6. 根据缺失类型应用相应的测试设计方法
7. 边界条件测试需包含具体的边界值数据
"""
logger.info("步骤3提示词长度: %d 字符", len(prompt))
return prompt
```
### 3.4 更新主流程
**文件:** `src/main.py`
```python
def main():
"""主函数 - 使用优化后的分步调用"""
logger.info("=" * 60)
logger.info("AI完善测试用例工具 启动(优化版本)")
logger.info("=" * 60)
# 检查API密钥
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
logger.error("未设置ANTHROPIC_API_KEY环境变量")
return 1
# 确保目录存在
from src.config import ensure_directories
ensure_directories()
try:
# 1. 读取文档
reader = DocumentReader()
requirements = reader.read_requirement_docs(REQUIREMENTS_DIR)
prds = reader.read_prd_docs(PRD_DIR)
test_cases = reader.read_test_cases(TEST_CASE_FILE)
logger.info("读取到需求文档 %d 份", len(requirements))
logger.info("读取到PRD文档 %d 份", len(prds))
logger.info("读取到测试用例 %d 条", len(test_cases))
# ========== 步骤1:提取功能点 ==========
logger.info("=" * 40)
logger.info("步骤1:提取功能点...")
analyzer = TestCaseAnalyzer(api_key)
function_points = analyzer.extract_function_points_v2(requirements, prds)
logger.info("提取到功能点 %d 个", len(function_points))
# ========== 步骤2:检测缺失 ==========
logger.info("=" * 40)
logger.info("步骤2:检测缺失...")
detector = GapDetector(api_key)
gaps = detector.detect_gaps_v2(function_points, test_cases)
logger.info("功能点缺失:%d 项", len(gaps.get("功能点缺失", [])))
logger.info("场景覆盖不足:%d 项", len(gaps.get("场景覆盖不足", [])))
logger.info("边界条件不足:%d 项", len(gaps.get("边界条件不足", [])))
# ========== 步骤3:生成用例 ==========
logger.info("=" * 40)
logger.info("步骤3:生成测试用例...")
generator = CaseGenerator(api_key)
new_cases = generator.generate_cases_v2(gaps, test_cases)
# ========== 后续处理 ==========
# 去重
deduplicator = Deduplicator()
dedup_result = deduplicator.deduplicate(test_cases, new_cases)
# 保存完善的测试用例
_save_perfected_cases(test_cases, dedup_result["unique_cases"])
# 生成差异性报告
_generate_report(gaps, dedup_result, function_points)
logger.info("=" * 60)
logger.info("处理完成!")
logger.info("=" * 60)
return 0
except Exception as e:
logger.error("处理失败: %s", e)
import traceback
logger.debug(traceback.format_exc())
return 1
```
---
## 4. 实施计划
### 4.1 实施步骤
| 序号 | 任务 | 预计时间 | 状态 |
|------|------|----------|------|
| 1 | 更新 `test_case_analyzer.py` - 添加 `extract_function_points_v2()` | 1h | ⏳ 待开始 |
| 2 | 更新 `gap_detector.py` - 添加 `detect_gaps_v2()` | 1h | ⏳ 待开始 |
| 3 | 更新 `case_generator.py` - 添加 `generate_cases_v2()` | 1.5h | ⏳ 待开始 |
| 4 | 更新 `main.py` - 使用新的分步调用流程 | 0.5h | ⏳ 待开始 |
| 5 | 测试验证 - 使用实际文档测试 | 1h | ⏳ 待开始 |
| 6 | 对比验证 - 对比优化前后效果 | 0.5h | ⏳ 待开始 |
**总计**:约 5.5 小时
### 4.2 兼容性说明
- 保留原有的方法(`extract_function_points``detect_gaps``generate_cases`
- 新增 `v2` 后缀的方法(`extract_function_points_v2``detect_gaps_v2``generate_cases_v2`
- 可以通过配置切换新旧版本
---
## 5. 验证标准
### 5.1 提示词长度验证
| 步骤 | 目标长度 | 验证方法 |
|------|----------|----------|
| 步骤1 | < 600字符 | 日志输出提示词长度 |
| 步骤2 | < 1000字符 | 日志输出提示词长度 |
| 步骤3 | < 800字符 | 日志输出提示词长度 |
### 5.2 功能验证
| 验证项 | 标准 |
|--------|------|
| 功能点提取 | 能正确提取所有功能点 |
| 缺失检测 | 能识别3类缺失,格式正确 |
| 用例生成 | 生成符合格式的测试用例 |
| JSON解析 | 所有AI返回都能正确解析 |
### 5.3 效果验证
| 指标 | 优化前 | 优化后目标 |
|------|--------|------------|
| 提示词总字符 | 6000 | < 2000 |
| 解析成功率 | ~80% | > 95% |
| 生成用例数量 | 偏少 | 明显增加 |
---
## 6. 注意事项
1. **向后兼容**:保留原有方法,新增v2版本
2. **日志记录**:每个步骤记录提示词长度
3. **错误处理**:单个步骤失败不影响其他步骤
4. **可配置性**:支持通过配置切换新旧版本
---
## 7. 后续优化
- [ ] 添加配置选项,支持新旧版本切换
- [ ] 添加步骤级别的重试机制
- [ ] 添加提示词效果监控(记录AI返回质量)
- [ ] 根据实际效果调优提示词内容
- [ ] 支持自定义每个步骤的提示词模板
# 问题描述
## 问题现象
- 执行代码转换英文后,文档中第一页的文本框内文字没有成功转换,文档可见[AuxiliaryTool/DocumentAutoOptimizationCalibration/reports/新统一平台自动化部署操作指导_英文版.docx]
\ No newline at end of file
# 文本框内的文字没有成功转换文字_问题处理_计划执行
## 1. 问题描述
### 问题现象
执行代码转换英文后,文档中第一页的文本框内文字没有成功转换。
### 问题文档
- 路径:`AuxiliaryTool/DocumentAutoOptimizationCalibration/reports/新统一平台自动化部署操作指导_英文版.docx`
### 预期结果
- 文档中所有文本框内的中文内容都应翻译为英文
### 实际结果
- 文本框内的中文内容没有被翻译
---
## 2. 问题分析
### 根本原因
1. **文本框未处理**:当前代码只处理段落、表格和页眉页脚,没有处理文本框
2. **文本框结构特殊**:文本框在Word文档中是绘图对象(Shape/TextBox),不在`doc.paragraphs`
3. **XML结构特殊**:文本框内容存储在文档的XML绘图部分,需要特殊处理
### 技术分析
当前 `internationalize_document()` 函数处理:
- `doc.paragraphs` - 正文段落
- `doc.tables` - 表格内容
- `section.header/footer` - 页眉页脚
但未处理:
- **文本框(TextBox)** - 通过 `w:txbxContent/w:p` 路径访问
- **绘图对象(Shape)** - 通过 `w:drawing``v:shape` 访问
- **SmartArt** - 复杂图形结构
### 文本框在Word文档中的结构
```
document.xml
└── body
└── p (段落)
└── r (run)
└── drawing/w:drawing
└── wp:inline 或 wp:anchor
└── a:graphic
└── a:graphicData
└── w:txbxContent (文本框内容)
└── w:p (文本框内的段落)
```
---
## 3. 解决方案
### 方案概述
1. 添加文本框检测和翻译函数
2. 遍历文档XML,查找所有文本框元素
3. 对每个文本框内的段落进行翻译
4. 保持文本框原有样式和格式
### 实现步骤
#### 步骤1:添加文本框检测函数
```python
def _find_all_textboxes(document):
"""
查找文档中所有文本框
Args:
document: Document对象
Yields:
文本框中的段落列表
"""
```
#### 步骤2:添加文本框翻译函数
```python
def _translate_textboxes(document, engine: str) -> int:
"""
翻译文档中所有文本框内容
Args:
document: Document对象
engine: 翻译引擎
Returns:
成功翻译的文本框段落数量
"""
```
#### 步骤3:集成到主函数
```python
def internationalize_document(input_path, output_path, engine):
# 现有段落处理...
# 现有表格处理...
# 现有页眉页脚处理...
# 新增:处理文本框
textbox_count = _translate_textboxes(doc, engine)
```
---
## 4. 实施计划
### 4.1 代码修改
- [ ] 修改 `src/internationalizer.py`
- [ ] 添加 `_find_all_textboxes()` 函数
- [ ] 添加 `_translate_textboxes()` 函数
- [ ] 修改 `internationalize_document()` 函数,添加文本框处理
- [ ] 添加详细调试日志
### 4.2 测试验证
- [ ] 使用问题文档测试
- [ ] 检查文本框内容是否翻译
- [ ] 验证文本框样式是否保留
### 4.3 文档更新
- [ ] 更新计划执行文档
---
## 5. 验证标准
### 成功标准
1. 所有文本框内的中文内容被翻译为英文
2. 文本框原有样式和格式保持不变
3. 日志清晰显示文本框处理过程
4. 不影响段落、表格和页眉页脚的翻译
---
## 6. 代码实现
### 新增函数:查找所有文本框
```python
def _find_all_textboxes(document):
"""
查找文档中所有文本框
在Word文档中,文本框存储在 w:txbxContent 元素中。
该函数遍历文档XML,查找所有文本框并返回其中的段落。
Args:
document: Document对象
Yields:
包含文本框段落的Element对象列表
"""
from docx.oxml.ns import qn
# 遍历文档正文的所有段落
for paragraph in document.paragraphs:
# 查找段落中的所有drawing元素
for drawing in paragraph._element.findall('.//w:drawing', document.nsmap):
# 查找文本框内容
for textbox in drawing.findall('.//w:txbxContent', document.nsmap):
yield textbox
# 查找段落中的所有legacy shape元素(旧版Word格式)
for shape in paragraph._element.findall('.//v:shape', document.nsmap):
# 旧版shape可能包含v:textbox
textbox = shape.find('.//v:textbox', document.nsmap)
if textbox is not None:
# 需要特殊处理legacy格式
yield textbox
```
### 新增函数:翻译文本框内容
```python
def _translate_textboxes(document, engine: str) -> int:
"""
翻译文档中所有文本框内容
Args:
document: Document对象
engine: 翻译引擎
Returns:
成功翻译的文本框段落数量
"""
from docx.oxml.ns import qn
from docx.text.paragraph import Paragraph
translated_count = 0
textbox_index = 0
for textbox_content in _find_all_textboxes(document):
textbox_index += 1
logger.debug("处理文本框 #%d", textbox_index)
# 获取文本框内的所有段落
paragraphs = textbox_content.findall('.//w:p', document.nsmap)
for p_element in paragraphs:
# 创建Paragraph对象以便处理
try:
# 直接操作XML元素
# 查找所有t:txbtRun或w:t元素(文本内容)
text_elements = p_element.findall('.//w:t', document.nsmap)
if not text_elements:
continue
# 获取原始文本
original_text = ''.join([t.text for t in text_elements if t.text])
if not original_text.strip():
continue
# 检查是否需要翻译
if not is_translatable(original_text):
logger.debug("文本框跳过(无需翻译): %s", original_text[:30])
continue
# 执行翻译
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
# 替换文本内容
# 简单处理:将所有文本替换为翻译后的文本
for i, t in enumerate(text_elements):
if i == 0:
t.text = translated_text
else:
t.text = ""
translated_count += 1
logger.info("文本框翻译[%d]: %s -> %s",
textbox_index, original_text[:30], translated_text[:30])
else:
logger.debug("文本框翻译失败或未翻译: %s", original_text[:30])
except Exception as e:
logger.warning("文本框段落翻译异常: %s", e)
except Exception as e:
logger.warning("文本框段落处理异常: %s", e)
logger.info("文本框翻译完成: 总数=%d, 翻译=%d", textbox_index, translated_count)
return translated_count
```
### 修改主函数
```python
def internationalize_document(input_path: str, output_path: Optional[str] = None,
engine: str = "google") -> str:
"""
文档国际化转换(主函数)
"""
# ... 现有段落处理 ...
# ... 现有表格处理 ...
# ... 现有页眉页脚处理 ...
# 新增:处理文本框
textbox_count = _translate_textboxes(doc, engine)
# ... 保存文档 ...
```
---
## 7. 后续优化
- [ ] 处理嵌套文本框
- [ ] 处理文本框内的表格
- [ ] 处理艺术字内容翻译
- [ ] 支持SmartArt内容翻译
- [ ] 添加翻译预览功能
---
## 8. 实施状态
| 项目 | 状态 | 说明 |
|------|------|------|
| 问题分析 | ✅ 完成 | 已分析根本原因 |
| 解决方案设计 | ✅ 完成 | 已设计解决方案 |
| 代码实现 | ✅ 完成 | 已实现文本框翻译功能 |
| 测试验证 | 待进行 | 需要使用问题文档测试 |
| 文档更新 | ✅ 完成 | 已更新文档 |
# 问题描述
## 问题现象
- 执行代码转换英文后,文档目录页中的内容没有同步转换,能否在文档翻译完成后自动更新目录域,文档可见[AuxiliaryTool/DocumentAutoOptimizationCalibration/reports/新统一平台自动化部署操作指导_英文版.docx]
\ No newline at end of file
# 目录页的目录内容没有同步转换_问题处理_计划执行
## 1. 问题描述
### 问题现象
执行代码转换英文后,文档目录页中的内容没有同步转换。
### 问题文档
- 路径:`AuxiliaryTool/DocumentAutoOptimizationCalibration/reports/新统一平台自动化部署操作指导_英文版.docx`
### 预期结果
- 文档翻译完成后,目录内容应自动更新为翻译后的标题
### 实际结果
- 目录内容仍显示原始中文标题,需要手动更新域
---
## 2. 问题分析
### 根本原因
#### 2.1 Word目录机制
1. **目录是域代码**:Word中的目录是通过 `{TOC}` 域代码自动生成的
2. **基于标题**:目录内容基于文档中的标题(Heading样式)
3. **不自动更新**:标题变化后,目录不会自动更新
#### 2.2 当前实现
当前代码处理流程:
1. 翻译所有段落(包括标题)
2. 翻译所有表格
3. 翻译页眉页脚
4. 保存文档
5. **仅提示用户手动更新目录**
#### 2.3 技术限制
- **python-docx限制**:python-docx库不支持更新Word域代码
- **目录保持原样**:翻译标题后,目录不会自动反映翻译内容
### 解决方案对比
| 方案 | 优点 | 缺点 | 推荐度 |
|------|------|------|--------|
| A. 使用win32com自动更新域 | 完全自动化,更新所有域 | 需要Windows+Word,需安装pywin32 | ⭐⭐⭐⭐ |
| B. 手动翻译目录段落 | 跨平台,无需额外依赖 | 页码可能不准确 | ⭐⭐⭐ |
| C. 生成VBA宏脚本 | 用户可手动执行 | 需要用户操作 | ⭐⭐ |
---
## 3. 解决方案
### 方案概述
实现**双轨制**解决方案:
1. **方案A(推荐)**:使用win32com自动更新所有域(目录、页码等)
2. **方案B(备选)**:手动翻译目录段落内容
### 实现步骤
#### 步骤1:添加目录段落翻译函数
直接翻译目录中的文字内容
#### 步骤2:添加win32com自动更新域函数
通过COM接口调用Word更新所有域
#### 步骤3:集成到主函数
在保存文档后执行目录更新
---
## 4. 实施计划
### 4.1 代码修改
- [ ] 修改 `src/internationalizer.py`
- [ ] 添加 `_find_toc_paragraphs()` 函数 - 查找目录段落
- [ ] 添加 `_translate_toc()` 函数 - 手动翻译目录
- [ ] 添加 `_update_fields_via_win32com()` 函数 - 使用win32com更新域
- [ ] 修改 `internationalize_document()` 函数 - 集成目录更新
### 4.2 依赖更新
- [ ] 在requirements.txt中添加pywin32(可选依赖)
### 4.3 测试验证
- [ ] 测试手动翻译目录
- [ ] 测试win32com自动更新域
- [ ] 验证页码正确性
### 4.4 文档更新
- [ ] 更新计划执行文档
- [ ] 更新README说明
---
## 5. 验证标准
### 成功标准
1. 目录内容显示翻译后的标题
2. 目录页码保持正确
3. 不需要用户手动操作
4. 兼容无Word环境
---
## 6. 代码实现
### 新增函数:查找目录段落
```python
def _find_toc_paragraphs(document):
"""
查找文档中的目录段落
Word目录通常包含特定特征:
1. 包含域代码指令 TOC ...
2. 或包含 "目录" 标题
3. 后面跟随的是标题条目
Args:
document: Document对象
Returns:
(toc_start_index, toc_end_index) - 目录开始和结束段落索引
"""
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
}
toc_start = -1
toc_end = -1
in_toc = False
for i, paragraph in enumerate(document.paragraphs):
text = paragraph.text.strip()
# 检测目录开始
if not in_toc:
# 方法1:查找包含"目录"的标题
if text in ['目录', '目 录', 'Contents', 'Table of Contents']:
if 'Heading' in paragraph.style.name or '标题' in paragraph.style.name:
toc_start = i
in_toc = True
continue
# 方法2:通过XML查找TOC域代码
for fldSimple in paragraph._element.findall('.//w:fldSimple', namespaces):
instr = fldSimple.get(f'{{{namespaces["w"]}}}instr')
if instr and 'TOC' in instr:
toc_start = i
in_toc = True
break
# 方法3:查找嵌套的域代码
for fldChar in paragraph._element.findall('.//w:fldChar', namespaces):
fldChar_type = fldChar.get(f'{{{namespaces["w"]}}}fldCharType')
if fldChar_type == 'begin':
# 查找后续的instrText
for sibling in paragraph._element.itersiblings():
instr_text = sibling.find('.//w:instrText', namespaces)
if instr_text is not None and instr_text.text and 'TOC' in instr_text.text:
toc_start = i
in_toc = True
break
# 检测目录结束
if in_toc:
# 目录通常在遇到下一个一级标题或分隔线时结束
if i > toc_start + 1: # 至少有目录标题
# 检查是否是下一个一级标题
if _is_heading_paragraph(paragraph) == 1:
toc_end = i
break
# 检查是否是分隔线(多个连续的=或-)
if text and all(c in '=-_—' for c in text):
toc_end = i
break
# 如果没有找到明确的结束位置,假设目录在5个段落内
if toc_start >= 0 and toc_end < 0:
toc_end = min(toc_start + 50, len(document.paragraphs))
return toc_start, toc_end
```
### 新增函数:手动翻译目录
```python
def _translate_toc_manual(document, engine: str) -> int:
"""
手动翻译目录内容
直接翻译目录段落中的文字,保持原有格式
Args:
document: Document对象
engine: 翻译引擎
Returns:
成功翻译的目录条目数量
"""
toc_start, toc_end = _find_toc_paragraphs(document)
if toc_start < 0:
logger.info("未检测到目录,跳过目录翻译")
return 0
logger.info("检测到目录位置: 段落 %d - %d", toc_start + 1, toc_end + 1)
translated_count = 0
# 翻译目录中的段落
for i in range(toc_start, min(toc_end + 1, len(document.paragraphs))):
paragraph = document.paragraphs[i]
original_text = paragraph.text.strip()
if not original_text:
continue
# 跳过目录标题本身
if original_text in ['目录', '目 录', 'Contents', 'Table of Contents']:
continue
# 检查是否需要翻译
if not is_translatable(original_text):
continue
# 目录条目通常包含页码,格式如 "标题....................1"
# 需要分离标题和页码
import re
toc_pattern = r'^(.+?)[\s\u3000\.。—_-]{1,}(\d+)$'
match = re.match(toc_pattern, original_text)
if match:
title_text = match.group(1).strip()
page_number = match.group(2)
# 只翻译标题部分
if is_translatable(title_text):
try:
translated_title = translate_text(
title_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_title and translated_title != title_text:
# 重建目录条目:翻译后的标题 + 分隔符 + 页码
# 尝试保持原有的分隔符
separator_match = re.match(r'^[\s\u3000\.。—_-]+', original_text[len(title_text):])
if separator_match:
separator = separator_match.group(0)
else:
separator = ' ' + '.' * 20 + ' '
new_text = translated_title + separator + page_number
# 清除原有runs
for run in paragraph.runs:
r = run._element
r.getparent().remove(r)
# 添加新文本
new_run = paragraph.add_run(new_text)
translated_count += 1
logger.debug("目录条目翻译: %s -> %s", title_text[:30], translated_title[:30])
except Exception as e:
logger.warning("目录条目翻译异常: %s", e)
else:
# 不符合目录条目格式的,尝试直接翻译
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
# 清除原有runs
for run in paragraph.runs:
r = run._element
r.getparent().remove(r)
# 添加新文本
new_run = paragraph.add_run(translated_text)
translated_count += 1
logger.debug("目录内容翻译: %s -> %s", original_text[:30], translated_text[:30])
except Exception as e:
logger.warning("目录内容翻译异常: %s", e)
logger.info("目录手动翻译完成: 翻译了 %d 个条目", translated_count)
return translated_count
```
### 新增函数:使用win32com自动更新域
```python
def _update_fields_via_win32com(doc_path: str) -> bool:
"""
使用win32com自动更新Word文档中的所有域
此功能需要:
1. Windows操作系统
2. 安装Microsoft Word
3. 安装pywin32库
Args:
doc_path: Word文档路径
Returns:
是否成功更新
"""
try:
import win32com.client
logger.info("使用win32com更新文档域...")
# 启动Word应用
word = win32com.client.Dispatch("Word.Application")
word.Visible = False # 不显示Word窗口
word.DisplayAlerts = 0 # 禁用警告
try:
# 打开文档
doc = word.Documents.Open(str(doc_path))
# 更新所有域
# wdFields = 1 # 更新所有域
doc.Fields.Update()
# 更新目录(如果有)
try:
doc.TablesOfContents(1).Update()
logger.info("目录已更新")
except Exception:
pass
# 更新页码域
try:
doc.Sections(1).Footers(1).PageNumbers.Update()
logger.info("页码域已更新")
except Exception:
pass
# 保存并关闭
doc.Save()
doc.Close()
logger.info("win32com域更新完成")
return True
finally:
# 退出Word应用
word.Quit()
except ImportError:
logger.warning("未安装pywin32库,无法使用win32com更新域")
logger.info("提示:安装命令:pip install pywin32")
return False
except Exception as e:
logger.error("win32com更新域失败: %s", e)
return False
```
### 修改主函数
```python
def internationalize_document(input_path: str, output_path: Optional[str] = None,
engine: str = "google", auto_update_fields: bool = True) -> str:
"""
文档国际化转换(主函数)
Args:
input_path: 输入文档路径
output_path: 输出文档路径(可选)
engine: 翻译引擎
auto_update_fields: 是否自动更新域(默认True)
"""
# ... 现有翻译处理 ...
# 保存翻译后的文档
doc.save(str(output_file))
logger.info("国际化转换完成: %s", output_file)
# 新增:处理目录更新
if auto_update_fields:
# 方案A:尝试使用win32com自动更新域
win32com_success = _update_fields_via_win32com(output_file)
if not win32com_success:
# 方案B:手动翻译目录
logger.info("win32com不可用,使用手动目录翻译...")
doc = Document(output_file)
toc_count = _translate_toc_manual(doc, engine)
if toc_count > 0:
doc.save(str(output_file))
logger.info("目录手动翻译完成: %d 个条目", toc_count)
else:
logger.info("提示:请在Word中打开文档后,右键点击目录并选择'更新域'以更新目录内容")
return str(output_file)
```
---
## 7. 后续优化
- [ ] 支持嵌套目录(多级目录)
- [ ] 支持图表目录、表格目录
- [ ] 添加目录格式自定义选项
- [ ] 支持Linux环境下的域更新
---
## 8. 实施状态
| 项目 | 状态 | 说明 |
|------|------|------|
| 问题分析 | ✅ 完成 | 已分析根本原因 |
| 解决方案设计 | ✅ 完成 | 已设计双轨制解决方案 |
| 代码实现 | ✅ 完成 | 已实现目录更新功能 |
| 语法验证 | ✅ 完成 | 代码语法检查通过 |
| 测试验证 | 待进行 | 需要测试不同场景 |
| 文档更新 | ✅ 完成 | 已更新文档 |
---
## 9. 使用说明
### 翻译命令
```bash
# 使用必应引擎翻译(推荐)
python run.py --translate --translate-engine bing --input "文档.docx"
# 使用百度引擎
python run.py --translate --translate-engine baidu --input "文档.docx"
# 禁用自动域更新(使用手动目录翻译)
python run.py --translate --input "文档.docx" --no-auto-update-fields
```
### win32com安装(可选)
```bash
# 如果想使用自动域更新功能,请安装pywin32
pip install pywin32
```
### 注意事项
1. **win32com方式**:需要Windows + 安装Word + pywin32,会更新所有域(目录、页码等)
2. **手动翻译方式**:跨平台兼容,但页码可能需要在Word中重新生成
3. **建议**:安装pywin32获得最佳体验
---
## 10. 代码修改记录
### 修改文件
- `src/internationalizer.py`
### 新增函数
```python
def _find_toc_paragraphs(document):
"""查找文档中的目录段落"""
def _translate_toc_manual(document, engine: str) -> int:
"""手动翻译目录内容"""
def _update_fields_via_win32com(doc_path: str) -> bool:
"""使用win32com自动更新Word文档中的所有域"""
```
### 修改内容
- `internationalize_document()` - 添加目录更新逻辑
# 问题描述
## 问题现象
- 执行代码转换英文后,文档页眉中文字没有成功转换,文档可见[AuxiliaryTool/DocumentAutoOptimizationCalibration/reports/新统一平台自动化部署操作指导_英文版.docx]
\ No newline at end of file
# 页眉中的文字没有成功转换_问题处理_计划执行
## 1. 问题描述
### 问题现象
执行代码转换英文后,文档页眉中文字没有成功转换。
### 问题文档
- 路径:`AuxiliaryTool/DocumentAutoOptimizationCalibration/reports/新统一平台自动化部署操作指导_英文版.docx`
### 预期结果
- 页眉中的所有中文内容都应翻译为英文
### 实际结果
- 页眉中的中文内容没有被翻译
---
## 2. 问题分析
### 根本原因
1. **页眉表格未处理**:当前代码 `_translate_header_footer_part()` 只处理了页眉中的段落,但页眉中可能包含表格
2. **页眉文本框未处理**:页眉中可能包含文本框,这些文本框也没有被处理
3. **页眉结构特殊**:很多文档使用表格来实现页眉的左右布局(如左侧标题,右侧日期)
### 技术分析
当前 `_translate_header_footer_part()` 函数处理:
- `header_footer.paragraphs` - 页眉页脚中的段落
但未处理:
- `header_footer.tables` - 页眉页脚中的表格
- 页眉中的文本框(w:txbxContent)
### 页眉中表格的常见用途
```
+------------------+------------------+
| 左侧内容 | 右侧内容 |
| (如文档标题) | (如日期/页码) |
+------------------+------------------+
```
---
## 3. 解决方案
### 方案概述
1. 扩展 `_translate_header_footer_part()` 函数,添加表格处理
2. 添加页眉文本框处理
3. 确保页眉所有类型的内容都能被翻译
### 实现步骤
#### 步骤1:添加页眉表格翻译
`_translate_header_footer_part()` 函数中添加表格处理逻辑
#### 步骤2:添加页眉文本框翻译
在页眉处理中添加文本框查找和翻译
#### 步骤3:改进日志输出
记录页眉中翻译的段落、表格和文本框数量
---
## 4. 实施计划
### 4.1 代码修改
- [ ] 修改 `src/internationalizer.py` 中的 `_translate_header_footer_part()` 函数
- [ ] 添加页眉表格处理逻辑
- [ ] 添加页眉文本框处理逻辑
- [ ] 改进日志输出
### 4.2 测试验证
- [ ] 使用问题文档测试
- [ ] 检查页眉表格内容是否翻译
- [ ] 检查页眉文本框内容是否翻译
### 4.3 文档更新
- [ ] 更新计划执行文档
---
## 5. 验证标准
### 成功标准
1. 页眉中的所有段落内容被翻译
2. 页眉中的表格单元格内容被翻译
3. 页眉中的文本框内容被翻译
4. 页眉原有样式和布局保持不变
5. 日志清晰显示页眉处理过程
---
## 6. 代码实现
### 修改后的 `_translate_header_footer_part()` 函数
```python
def _translate_header_footer_part(header_footer, engine: str,
translated_filename: str = None,
original_filename: str = None) -> int:
"""
翻译页眉或页脚中的段落、表格和文本框内容
Args:
header_footer: 页眉或页脚对象
engine: 翻译引擎
translated_filename: 翻译后的文件名(用于页眉)
original_filename: 原文件名(用于判断是否需要替换)
Returns:
成功翻译的内容数量(段落+表格单元格+文本框)
"""
translated_count = 0
table_cell_count = 0
textbox_count = 0
# 判断是否为页眉
is_header = False
try:
from docx.oxml.ns import qn
if hasattr(header_footer, '_element'):
parent = header_footer._element.getparent()
if parent is not None:
is_header = 'header' in parent.tag
except Exception:
pass
# 处理页眉页脚中的段落
for paragraph in header_footer.paragraphs:
if not paragraph.text.strip():
continue
original_text = paragraph.text
# 如果是页眉且内容是原文件名,使用翻译后的文件名
if is_header and translated_filename and original_filename:
if original_filename in original_text or original_text == original_filename:
# 清除原有runs
for run in paragraph.runs:
r = run._element
r.getparent().remove(r)
# 设置翻译后的文件名
new_run = paragraph.add_run(translated_filename)
# 设置字体样式
try:
from src.optimizer import _set_run_font_style, HEADER_FOOTER_FONT_SIZE
_set_run_font_style(new_run, "宋体", HEADER_FOOTER_FONT_SIZE, bold=False, color="000000")
except Exception:
pass
translated_count += 1
logger.info("页眉文件名翻译: %s -> %s", original_text, translated_filename)
continue
# 检查是否需要翻译
if not is_translatable(original_text):
logger.debug("页眉页脚跳过(无需翻译): %s", original_text[:30])
continue
try:
# 执行翻译
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
# 清除原有runs
for run in paragraph.runs:
r = run._element
r.getparent().remove(r)
# 添加新文本
new_run = paragraph.add_run(translated_text)
translated_count += 1
logger.info("页眉页脚翻译: %s -> %s",
original_text[:30], translated_text[:30])
except Exception as e:
logger.warning("页眉页脚翻译异常: %s", e)
# 新增:处理页眉页脚中的表格
for table in header_footer.tables:
for row in table.rows:
for cell in row.cells:
if _translate_table_cell(cell, engine):
table_cell_count += 1
if table_cell_count > 0:
logger.info("页眉页脚表格翻译: %d 个单元格", table_cell_count)
translated_count += table_cell_count
# 新增:处理页眉页脚中的文本框
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
'v': 'urn:schemas-microsoft-com:vml'
}
for paragraph in header_footer.paragraphs:
# 查找段落中的文本框
for drawing in paragraph._element.findall('.//w:drawing', namespaces):
for textbox in drawing.findall('.//w:txbxContent', namespaces):
# 处理文本框内的段落
for p_element in textbox.findall('.//w:p', namespaces):
text_elements = p_element.findall('.//w:t', namespaces)
if text_elements:
original_text = ''.join([t.text for t in text_elements if t.text])
if original_text.strip() and is_translatable(original_text):
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
for i, t in enumerate(text_elements):
if i == 0:
t.text = translated_text
else:
t.text = ""
textbox_count += 1
logger.info("页眉页脚文本框翻译: %s -> %s",
original_text[:30], translated_text[:30])
except Exception as e:
logger.warning("页眉页脚文本框翻译异常: %s", e)
# 查找旧版格式的文本框
for shape in paragraph._element.findall('.//v:shape', namespaces):
textbox = shape.find('.//v:textbox', namespaces)
if textbox is not None:
txbx_content = textbox.find('.//w:txbxContent', namespaces)
if txbx_content is not None:
for p_element in txbx_content.findall('.//w:p', namespaces):
text_elements = p_element.findall('.//w:t', namespaces)
if text_elements:
original_text = ''.join([t.text for t in text_elements if t.text])
if original_text.strip() and is_translatable(original_text):
try:
translated_text = translate_text(
original_text,
engine=engine,
from_lang=SOURCE_LANGUAGE,
to_lang=TARGET_LANGUAGE
)
if translated_text and translated_text != original_text:
for i, t in enumerate(text_elements):
if i == 0:
t.text = translated_text
else:
t.text = ""
textbox_count += 1
logger.info("页眉页脚文本框翻译(旧版): %s -> %s",
original_text[:30], translated_text[:30])
except Exception as e:
logger.warning("页眉页脚文本框翻译异常(旧版): %s", e)
if textbox_count > 0:
logger.info("页眉页脚文本框翻译: %d 个", textbox_count)
translated_count += textbox_count
return translated_count
```
---
## 7. 后续优化
- [ ] 处理页眉中的SmartArt
- [ ] 处理页眉中的艺术字
- [ ] 支持首页不同页眉的翻译
- [ ] 支持奇偶页不同页眉的翻译
---
## 8. 实施状态
| 项目 | 状态 | 说明 |
|------|------|------|
| 问题分析 | ✅ 完成 | 已分析根本原因 |
| 解决方案设计 | ✅ 完成 | 已设计解决方案 |
| 代码实现 | ✅ 完成 | 已扩展页眉页脚翻译功能 |
| 语法验证 | ✅ 完成 | 代码语法检查通过 |
| 测试验证 | 待进行 | 需要使用问题文档测试 |
| 文档更新 | ✅ 完成 | 已更新文档 |
---
## 9. 测试结果
### 测试步骤
1. 准备包含表格页眉的文档
2. 执行翻译命令
3. 检查页眉中的表格内容是否被翻译
### 预期结果
- 页眉中的段落被翻译
- 页眉中的表格单元格内容被翻译
- 页眉中的文本框内容被翻译
- 页眉布局和样式保持不变
### 测试命令
```bash
cd AuxiliaryTool/DocumentAutoOptimizationCalibration
python run.py --translate --translate-engine bing --input "testcases/文档.docx"
```
# 需求文档
## 需求背景与目标
### 1.1 背景
当前自动化测试用例仍是手动编写,效率较低,且无法批量生成,且无法自动生成JSON数据模板,无法自动生成测试用例。需要实现自动化测试用例生成。
### 1.2 目标
通过claude code+mcp,根据system_config.json获取系统登录地址、账号密码。根据提供的module_config.json自动访问系统的对应模块进行操作,收集操作步骤的元素定位类型以及元素定位值,并通过自动化测试用例JSON数据模板输出自动化测试用例。实现快速自动补充JSON数据
## 2. 功能需求
1. 根据同目录下的config.json文件,获取系统登录地址、账号密码。
- system_config.json文件格式示例:
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
```
2. 根据同目录下的module_config.json文件,获取需要收集的自动化模块名称、模块功能。
- module_config.json文件格式示例:
```json
[
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
},
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
}
]
```
3. 通过claude code+mcp,自动访问系统进行模块操作,并通过自动化测试用例JSON数据模板输出自动化测试用例。
- 自动化测试用例JSON数据模板格式示例:
```json
{
"name": "{module_name}_{module_name_son}_{model_function}",
"para": [
{
"page": "login/logindf",
"step": "点击左侧【用户管理】",
"locator_type": "XPATH",
"locator_value": "//span[contains(.,'用户管理')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "点击【用户列表】按钮",
"locator_type": "XPATH",
"locator_value": "//li[contains(.,'用户列表')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "点击【新增】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(.,'添加')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入账号",
"locator_type": "XPATH",
"locator_value": "//input[@id='accountChange']",
"element_type": "input",
"element_value": "admin@zdh",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入用户名",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='用户名']",
"element_type": "input",
"element_value": "admin@zdh",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入新密码",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='11位及以上的大小写字母和数字且连续3位及以上不重复和不连续组合']",
"element_type": "input",
"element_value": "Ubains@1357",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "输入确认密码",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='确认密码']",
"element_type": "input",
"element_value": "Ubains@1357",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "点击【确定】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(.,'确定')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "login/logindf",
"step": "获取提示文本",
"locator_type": "XPATH",
"locator_value": "//p[@class='el-message__content']",
"element_type": "getTips",
"element_value": "",
"expected_result": "添加成功"
},
{
"page": "login/logindf",
"step": "验证列表数据",
"locator_type": "XPATH",
"locator_value": "//td[contains(.,'admin@zdh')]",
"element_type": "getText",
"element_value": "",
"expected_result": "列表中包含新添加的用户"
}
],
"platform": "web",
"base_url": "https://192.168.5.44/"
}
```
- 参数说明:
- name: 测试用例名称,命名格式为:{module_name}_{module_name_son}_{model_function},例如:区域管理_增值服务_添加。
- para: 测试步骤列表,每个步骤包含以下字段:
- page: page字段为当前模块功能操作的页面路由后缀。
- step: 操作步骤描述,例如:点击左侧【用户管理】。
- locator_type: 元素定位类型,例如:ID、XPATH、CSS_SELECTOR等。优先获取ID、XPATH,**不允许用UID**。
- locator_value: 元素定位值,例如://button[contains(.,'确定')],需要与locator_type一致。
- **注意**:XPATH中使用 `contains(.,'文本')` 而非 `contains(text(),'文本')`,以正确匹配Element UI等框架中span子元素内的文本。
- element_type: 操作类型,例如:click、input、select、checkbox、switch、getTips、getText。
- element_value: 操作值,**根据element_type不同有不同填写规则**:
- `input`:填写需输入的文本内容(如"test001"
- `getTips`:**留空**,预期结果填写在expected_result字段
- `getText`:**留空**,预期结果填写在expected_result字段
- `click`、`select`、`checkbox`、`switch`:**留空**
- expected_result: 预期结果描述,根据操作类型填写,如"添加成功""列表中包含新添加的数据"等。
4. 输出的自动化测试用例文件命名格式为:{module_name}_{module_name_son}_{model_function}.json,例如:区域管理_增值服务_添加.json。
- 模板参考:[自动化测试用例模板](新统一平台/测试数据/新统一平台测试用例 - 副本.xlsx)
- 输出位置:config文件同目录下的 `testcases/` 子目录
## 3. 需求补充说明
### 3.1 配置文件格式规范
#### 3.1.1 system_config.json格式
采用标准JSON格式,包含以下字段:
- `system_type`: 系统类型标识
- `system_front_url`: 系统前台地址
- `system_back_url`: 系统后台地址
- `username`: 登录账号
- `password`: 登录密码
- `code`: 验证码(固定值:csba)
**注意**:登录时需用户输入选择使用前台或后台URL。
#### 3.1.2 module_config.json格式
- `module_name`: 一级菜单名称
- `module_name_son`: 二级菜单名称
- `module_function`: 功能列表,支持"添加""编辑""删除"
### 3.2 执行流程规范
#### 3.2.1 模块导航方式
采用**两级菜单导航**:
1. 先点击一级菜单(`module_name`)
2. 再点击二级菜单(`module_name_son`)
#### 3.2.2 功能操作策略
- **添加操作**:填写通用固定测试值
- **编辑操作**:先自动创建测试数据 执行编辑 清理测试数据
- **删除操作**:先自动创建测试数据 执行删除 清理测试数据
#### 3.2.3 元素定位策略
采用**智能检测**方式,按以下优先级选择定位方式:
1. **优先级1**:id属性(唯一标识)
2. **优先级2**:name属性
3. **优先级3**:class属性
4. **备选方案**:XPATH表达式
#### 3.2.4 Page字段获取
通过**URL解析**获取当前功能所在页面的路由后缀。
#### 3.2.5 预期结果生成
根据操作类型**自动生成**预期结果提示文本,例如:
- 添加操作 "添加成功"
- 编辑操作 "编辑成功"
- 删除操作 "删除成功"
### 3.3 支持的元素类型(element_type)
#### 3.3.1 基础类型
- **click**:点击按钮、链接等可点击元素,如按钮元素,则用`span`标签中的文本进行定位(如Element UI的button组件中的span子元素)
- **input**:文本输入框(input type=text)
- **select**:下拉选择框(select)
#### 3.3.2 选择类型
- **checkbox**:复选框(checkbox)和单选框(radio)
- **switch**:开关控件(如Element UI的switch)
#### 3.3.3 验证类型
- **getTips**:获取弹窗提示信息(如Element UI的el-message组件中的"添加成功""删除成功"等提示),元素值固定为:`"//p[@class='el-message__content']"`
- **getText**:获取列表中的文本数据(如表格td元素中的文本,用于验证数据是否存在于列表中)
### 3.4 element_value expected_result 填写规范
#### 3.4.1 element_value 填写规则
| element_type | element_value 填写内容 | 示例 |
|-------------|----------------------|------|
| `input` | 需输入的文本内容 | `"test001"` |
| `getTips` | **留空** | `""` |
| `getText` | **留空** | `""` |
| `click` | **留空** | `""` |
| `select` | **留空** | `""` |
| `checkbox` | **留空** | `""` |
| `switch` | **留空** | `""` |
#### 3.4.2 expected_result 填写规则
| element_type | expected_result 填写内容 | 示例 |
|-------------|-------------------------|------|
| `input` | 通常留空(非验证步骤) | `""` |
| `click` | 通常留空(非验证步骤) | `""` |
| `select` | 通常留空(非验证步骤) | `""` |
| `getTips` | 预期的弹窗提示文本 | `"添加成功"` |
| `getText` | 预期的验证描述 | `"列表中包含新添加的商品"` |
#### 3.4.3 完整示例对比
**错误的写法**(element_value填写了验证相关内容):
```json
{
"element_type": "getTips",
"element_value": "添加成功", // ❌ 错误
"expected_result": ""
}
```
**正确的写法**(验证内容填写在expected_result):
```json
{
"element_type": "getTips",
"element_value": "", // ✅ 正确:留空
"expected_result": "添加成功" // ✅ 正确:填写预期结果
}
```
### 3.5 测试数据规范
#### 3.4.1 固定测试值
使用**通用测试值**,例如:
- 用户名:test001
- 手机号:13800138000
- 密码:Test@123456
- 邮箱:test001@example.com
### 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
*文档结束*
\ No newline at end of file
# _PRD_自动化测试用例生成需求文档_计划执行
> 版本:V1.0
> 创建日期:2026-03-06
> 更新日期:2026-03-06
> 适用范围:自动化测试用例生成工具
> 来源:基于《_PRD_自动化测试用例生成需求文档.md》
> 状态:待执行
---
## 1. 项目概述
### 1.1 背景
当前自动化测试用例仍是手动编写,效率较低,且无法批量生成JSON数据模板。需要通过Claude Code + MCP技术,实现自动化测试用例的自动生成,提高测试用例编写效率。
### 1.2 目标
- 使用chrome-devtools MCP工具自动访问系统
- 根据配置文件自动执行模块操作并收集元素定位信息
- 按照JSON模板自动生成测试用例文件
- 支持添加、编辑、删除三种操作的自动化生成
- 实现快速批量生成测试用例
### 1.3 技术栈
- **Claude Code**: AI代码生成与执行
- **chrome-devtools MCP**: 浏览器自动化工具
- **Python**: 主要开发语言
- **JSON**: 配置文件和输出格式
### 1.4 涉及文件
**输入文件:**
- `system_config.json` - 系统配置文件(登录信息)
- `module_config.json` - 模块配置文件(测试模块和功能)
**输出文件:**
- `{module_name}_{module_name_son}_{function}.json` - 生成的测试用例文件
- 输出目录:config文件同目录下的 `testcases/` 子目录
**需要创建的模块文件:**
| 序号 | 模块名称 | 文件名 | 职责 | 状态 |
|-----|---------|--------|------|------|
| 1 | 配置管理模块 | config_manager.py | 读取和解析配置文件 | ⏳ 待创建 |
| 2 | 浏览器操作模块 | browser_operator.py | 封装chrome-devtools MCP操作 | ⏳ 待创建 |
| 3 | 登录模块 | login_handler.py | 处理系统登录 | ⏳ 待创建 |
| 4 | 导航模块 | navigation_handler.py | 处理菜单导航 | ⏳ 待创建 |
| 5 | 元素定位模块 | element_locator.py | 智能检测元素定位方式 | ⏳ 待创建 |
| 6 | 操作执行模块 | operation_executor.py | 执行添加/编辑/删除操作 | ⏳ 待创建 |
| 7 | 用例生成模块 | testcase_generator.py | 生成JSON测试用例 | ⏳ 待创建 |
| 8 | 主控模块 | main.py | 统一调度各模块 | ⏳ 待创建 |
---
## 2. 功能需求分析
### 2.1 核心功能流程
```
主程序启动
├── 1) 读取system_config.json获取系统信息
├── 2) 读取module_config.json获取测试模块
├── 3) 用户选择使用前台或后台
├── 4) 初始化chrome-devtools MCP连接
├── 5) 登录系统(固定验证码csba)
├── 6) 遍历module_config中的模块配置
│ ├── 6.1) 两级菜单导航(module_name → module_name_son)
│ ├── 6.2) 执行功能操作
│ │ ├── 添加操作:填写固定测试值
│ │ ├── 编辑操作:创建→编辑→清理
│ │ └── 删除操作:创建→删除→清理
│ ├── 6.3) 智能检测元素定位
│ ├── 6.4) 从URL解析page字段
│ ├── 6.5) 自动生成expected_result
│ └── 6.6) 生成JSON测试用例文件
└── 7) 输出到testcases/目录
```
### 2.2 配置文件规范
#### 2.2.1 system_config.json格式
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
```
#### 2.2.2 module_config.json格式
```json
[
{
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加", "编辑", "删除"]
}
]
```
### 2.3 测试用例JSON模板
```json
{
"name": "{module_name}_{module_name_son}_{function}",
"para": [
{
"page": "页面路由",
"step": "操作步骤描述",
"locator_type": "ID/XPATH/CSS_SELECTOR",
"locator_value": "元素定位值",
"element_type": "click/input/select/checkbox/switch/getTips/getText",
"element_value": "操作值",
"expected_result": "预期结果"
}
],
"platform": "web",
"base_url": "系统URL"
}
```
**参数填写规范:**
| 参数 | 说明 | 填写规则 |
|------|------|----------|
| `locator_type` | 元素定位类型 | 优先使用ID、XPATH;**不允许使用UID** |
| `locator_value` | 元素定位值 | XPATH使用 `contains(.,'text')` 而非 `contains(text(),'text')` |
| `element_type` | 操作类型 | click/input/select/checkbox/switch/getTips/getText |
| `element_value` | 操作值 | 根据element_type填写(详见下文规范) |
| `expected_result` | 预期结果 | 根据element_type填写(详见下文规范) |
---
## 3. 模块详细设计
### 3.1 config_manager.py - 配置管理模块
**职责:**
- 读取system_config.json文件
- 读取module_config.json文件
- 验证配置文件格式
- 提供配置数据访问接口
**导出函数:**
```python
def load_system_config(config_path: str) -> List[Dict]
def load_module_config(config_path: str) -> List[Dict]
def validate_system_config(config: Dict) -> bool
def validate_module_config(config: Dict) -> bool
```
**依赖:**
- Python标准库:json、pathlib
---
### 3.2 browser_operator.py - 浏览器操作模块
**职责:**
- 封装chrome-devtools MCP工具调用
- 提供统一的浏览器操作接口
- 处理页面快照和元素查找
- 错误处理和重试机制
**导出函数:**
```python
class BrowserOperator:
def __init__(self)
def open_page(self, url: str) -> bool
def take_snapshot(self) -> Dict
def find_element(self, text: str) -> str
def click_element(self, uid: str) -> bool
def fill_input(self, uid: str, value: str) -> bool
def select_option(self, uid: str, value: str) -> bool
def get_url(self) -> str
def wait_for_element(self, text: str, timeout: int) -> bool
```
**依赖:**
- chrome-devtools MCP工具
- mcp__chrome_devtools_* 系列函数
---
### 3.3 login_handler.py - 登录模块
**职责:**
- 处理系统登录流程
- 填写用户名、密码、验证码
- 处理登录后的页面状态
- 验证登录成功
**导出函数:**
```python
class LoginHandler:
def __init__(self, browser: BrowserOperator)
def login(self, url: str, username: str, password: str, code: str) -> bool
def verify_login_success(self) -> bool
```
**登录流程:**
1. 打开登录页面
2. 查找用户名输入框并填写
3. 查找密码输入框并填写
4. 查找验证码输入框并填写(固定csba)
5. 查找登录按钮并点击
6. 等待页面跳转
7. 验证登录成功
**依赖:**
- browser_operator模块
---
### 3.4 navigation_handler.py - 导航模块
**职责:**
- 处理两级菜单导航
- 根据module_name和module_name_son定位菜单
- 处理菜单展开和点击
- 验证导航成功
**导出函数:**
```python
class NavigationHandler:
def __init__(self, browser: BrowserOperator)
def navigate_to_module(self, module_name: str, module_name_son: str) -> bool
def find_menu_item(self, menu_text: str) -> str
def expand_menu(self, menu_uid: str) -> bool
def click_submenu(self, submenu_text: str) -> bool
def get_current_route(self) -> str
```
**导航流程:**
1. 获取页面快照
2. 查找一级菜单(module_name)
3. 如果菜单未展开,点击展开
4. 查找二级菜单(module_name_son)
5. 点击二级菜单
6. 等待页面加载
7. 从URL解析并返回路由
**依赖:**
- browser_operator模块
---
### 3.5 element_locator.py - 元素定位模块
**职责:**
- 智能检测元素定位方式
- 按优先级选择最优定位策略
- 生成多种定位方式作为备选
- 记录元素属性信息
**导出函数:**
```python
class ElementLocator:
def __init__(self, browser: BrowserOperator)
def locate_element(self, element_info: Dict) -> Dict
def get_locator_strategy(self, element: Dict) -> str
def generate_locator_value(self, element: Dict, strategy: str) -> str
def analyze_element_attributes(self, uid: str) -> Dict
```
**定位策略优先级:**
1. **ID属性**:如果元素有唯一id,优先使用
2. **Name属性**:如果元素有唯一name
3. **Class属性**:如果元素有唯一class
4. **XPATH表达式**:作为备选方案
- **注意****不允许使用UID**作为定位类型
- **注意**:XPATH中使用 `contains(.,'text')` 而非 `contains(text(),'text')`,以正确匹配Element UI等框架中span子元素内的文本
**输出格式:**
```python
{
"locator_type": "ID/XPATH/CSS_SELECTOR",
"locator_value": "//input[@id='username']",
"backup_locators": [...] # 备选定位方式
}
```
**依赖:**
- browser_operator模块
---
### 3.6 operation_executor.py - 操作执行模块
**职责:**
- 执行添加操作
- 执行编辑操作(创建→编辑→清理)
- 执行删除操作(创建→删除→清理)
- 记录操作步骤和元素信息
- 填写固定测试值
**导出函数:**
```python
class OperationExecutor:
def __init__(self, browser: BrowserOperator, locator: ElementLocator)
def execute_add(self) -> List[Dict]
def execute_edit(self) -> List[Dict]
def execute_delete(self) -> List[Dict]
def fill_test_data(self, form_elements: List[Dict]) -> None
def click_operation_button(self, operation: str) -> bool
def create_test_data(self) -> bool
def cleanup_test_data(self) -> bool
```
**固定测试值:**
```python
TEST_DATA = {
"username": "test001",
"password": "Test@123456",
"phone": "13800138000",
"email": "test001@example.com",
"name": "测试用户",
"description": "测试内容"
}
```
**操作流程:**
**添加操作:**
1. 点击"添加"按钮
2. 等待表单弹窗
3. 收集表单字段
4. 填写固定测试值
5. 点击"确定"按钮
6. **获取操作提示**(getTips - 弹窗提示)
7. **验证列表数据**(getText - 列表数据验证)
8. 记录所有步骤
**编辑操作:**
1. 执行添加操作创建测试数据
2. 点击"编辑"按钮
3. 修改部分字段
4. 点击"确定"按钮
5. **获取操作提示**(getTips - 弹窗提示)
6. **验证列表数据**(getText - 列表数据验证)
7. 删除测试数据
**删除操作:**
1. 执行添加操作创建测试数据
2. 点击"删除"按钮
3. 确认删除
4. **获取操作提示**(getTips - 弹窗提示)
5. **验证列表数据**(getText - 列表数据验证)
**依赖:**
- browser_operator模块
- element_locator模块
---
### 3.7 testcase_generator.py - 用例生成模块
**职责:**
- 根据操作步骤生成JSON测试用例
- 生成测试用例名称
- 填充page、step、expected_result字段
- 输出JSON文件到指定目录
**导出函数:**
```python
class TestCaseGenerator:
def __init__(self, module_info: Dict, base_url: str)
def generate_testcase(self, operation: str, steps: List[Dict]) -> Dict
def generate_name(self, operation: str) -> str
def generate_expected_result(self, operation: str) -> str
def save_testcase(self, testcase: Dict, output_dir: str) -> str
def format_json(self, data: Dict) -> str
```
**预期结果生成规则:**
```python
EXPECTED_RESULTS = {
"添加": "添加成功",
"编辑": "编辑成功",
"删除": "删除成功",
"提交": "提交成功",
"保存": "保存成功"
}
```
**依赖:**
- Python标准库:json、pathlib
---
### 3.8 main.py - 主控模块
**职责:**
- 统一调度各模块
- 处理用户输入(前台/后台选择)
- 控制整体执行流程
- 错误处理和日志记录
**导出函数:**
```python
class TestCaseGeneratorMain:
def __init__(self, system_config: str, module_config: str)
def run(self) -> None
def select_login_url(self, system: Dict) -> str
def process_modules(self) -> None
def process_single_module(self, module: Dict) -> None
def log(self, message: str, level: str = "INFO") -> None
```
**主流程:**
```python
def main():
# 1. 加载配置
system_config = load_system_config("system_config.json")
module_config = load_module_config("module_config.json")
# 2. 选择登录URL
login_url = select_login_url(system_config[0])
# 3. 初始化浏览器
browser = BrowserOperator()
browser.open_page(login_url)
# 4. 登录系统
login_handler = LoginHandler(browser)
login_handler.login(...)
# 5. 处理每个模块
for module in module_config:
process_single_module(module)
# 6. 输出报告
print("测试用例生成完成!")
```
**依赖:**
- 所有其他模块
---
## 4. 目录结构设计
```
ubains-module-test/
├── AuxiliaryTool/
│ └── TestCaseGenerator/ # 测试用例生成工具目录
│ ├── main.py # 主程序入口
│ ├── config_manager.py # 配置管理模块
│ ├── browser_operator.py # 浏览器操作模块
│ ├── login_handler.py # 登录模块
│ ├── navigation_handler.py # 导航模块
│ ├── element_locator.py # 元素定位模块
│ ├── operation_executor.py # 操作执行模块
│ ├── testcase_generator.py # 用例生成模块
│ ├── config/ # 配置文件目录
│ │ ├── system_config.json
│ │ └── module_config.json
│ ├── testcases/ # 输出测试用例目录
│ │ ├── 区域管理_增值服务_添加.json
│ │ ├── 区域管理_增值服务_编辑.json
│ │ └── 区域管理_增值服务_删除.json
│ └── utils/ # 工具函数
│ ├── __init__.py
│ ├── logger.py # 日志工具
│ └── constants.py # 常量定义
```
---
## 5. 支持的元素类型
### 5.1 基础类型
| element_type | 说明 | 操作方式 | 示例 |
|-------------|------|---------|------|
| click | 点击按钮、链接等 | 点击元素 | 点击【确定】按钮 |
| input | 文本输入框 | 输入文本 | 输入用户名 |
| select | 下拉选择框 | 选择选项 | 选择状态【启用】 |
### 5.2 选择类型
| element_type | 说明 | 操作方式 | 示例 |
|-------------|------|---------|------|
| checkbox | 复选框/单选框 | 勾选/取消勾选 | 勾选【启用】复选框 |
| switch | 开关控件 | 打开/关闭 | 打开【自动同步】开关 |
### 5.3 验证类型
| element_type | 说明 | 操作方式 | element_value | expected_result | 示例 |
|-------------|------|---------|-------------|----------------|------|
| getTips | 获取弹窗提示信息 | 读取Element UI等组件的提示文本 | 留空 `""` | 填写预期提示文本 | `"添加成功"` |
| getText | 获取列表数据文本 | 读取表格td等元素中的文本 | 留空 `""` | 填写验证描述 | `"列表中包含新添加的数据"` |
---
## 5.5 element_value 和 expected_result 填写规范
### 5.5.1 element_value 填写规则
| element_type | element_value 填写内容 | 示例 |
|-------------|----------------------|------|
| `input` | 填写需输入的文本内容 | `"test001"` |
| `getTips` | **留空** `""` | `""` |
| `getText` | **留空** `""` | `""` |
| `click` | **留空** `""` | `""` |
| `select` | **留空** `""` | `""` |
| `checkbox` | **留空** `""` | `""` |
| `switch` | **留空** `""` | `""` |
### 5.5.2 expected_result 填写规则
| element_type | expected_result 填写内容 | 示例 |
|-------------|-------------------------|------|
| `input` | 通常留空(非验证步骤) | `""` |
| `click` | 通常留空(非验证步骤) | `""` |
| `select` | 通常留空(非验证步骤) | `""` |
| `getTips` | 填写预期的弹窗提示文本 | `"添加成功"` |
| `getText` | 填写预期的验证描述 | `"列表中包含新添加的商品"` |
### 5.5.3 完整示例对比
**错误的写法**(element_value填写了验证相关内容):
```json
{
"element_type": "getTips",
"element_value": "添加成功", // 错误
"expected_result": ""
}
```
**正确的写法**(验证内容填写在expected_result):
```json
{
"element_type": "getTips",
"element_value": "", // 正确:留空
"expected_result": "添加成功" // 正确:填写预期结果
}
```
### 5.5.4 XPath 定位规范
**错误的写法**(无法匹配Element UI按钮span子元素内的文本):
```json
{
"locator_value": "//button[contains(text(),'确定')]" // 错误
}
```
**正确的写法**(使用 `contains(.,'text')` 匹配整个元素文本内容):
```json
{
"locator_value": "//button[contains(.,'确定')]" // 正确
}
```
**多条件定位的简化**(移除不必要的多条件组合):
```json
// 错误:多条件组合,定位器过长
"locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
// 正确:简化为唯一条件
"locator_value": "//button[contains(.,'确定')]"
```
---
## 6. 执行计划
### 6.1 阶段划分
| 阶段 | 任务 | 预计工作量 | 状态 |
|-----|------|----------|------|
| **阶段1** | 创建项目目录结构 | 0.5天 | ⏳ 待执行 |
| **阶段2** | 实现config_manager模块 | 0.5天 | ⏳ 待执行 |
| **阶段3** | 实现browser_operator模块 | 1-2天 | ⏳ 待执行 |
| **阶段4** | 实现login_handler模块 | 1天 | ⏳ 待执行 |
| **阶段5** | 实现navigation_handler模块 | 1天 | ⏳ 待执行 |
| **阶段6** | 实现element_locator模块 | 1-2天 | ⏳ 待执行 |
| **阶段7** | 实现operation_executor模块 | 2-3天 | ⏳ 待执行 |
| **阶段8** | 实现testcase_generator模块 | 1天 | ⏳ 待执行 |
| **阶段9** | 实现main主控模块 | 1天 | ⏳ 待执行 |
| **阶段10** | 集成测试 | 2-3天 | ⏳ 待执行 |
| **阶段11** | 文档编写 | 1天 | ⏳ 待执行 |
### 6.2 里程碑
| 里程碑 | 完成标准 | 状态 |
|-------|---------|------|
| M1: 基础模块完成 | config_manager、browser_operator、login_handler 完成 | ⏳ 待达成 |
| M2: 核心功能完成 | navigation、element_locator、operation_executor 完成 | ⏳ 待达成 |
| M3: 生成功能完成 | testcase_generator、main 完成 | ⏳ 待达成 |
| M4: 测试完成 | 端到端测试通过 | ⏳ 待达成 |
---
## 7. 测试计划
### 7.1 单元测试
每个模块需要测试:
- 函数正常执行
- 参数校验
- 异常处理
- 返回值格式
### 7.2 集成测试
测试场景:
1. 新平台系统完整流程
2. 多模块批量生成
3. 不同元素类型处理
4. 添加/编辑/删除操作
5. JSON输出格式验证
### 7.3 测试用例示例
**输入配置:**
```json
{
"module_name": "用户管理",
"module_name_son": "用户列表",
"module_function": ["添加"]
}
```
**预期输出:**
```json
{
"name": "用户管理_用户列表_添加",
"para": [
{
"page": "system/user",
"step": "点击左侧【用户管理】",
"locator_type": "XPATH",
"locator_value": "//span[contains(.,'用户管理')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "点击【用户列表】",
"locator_type": "XPATH",
"locator_value": "//li[contains(.,'用户列表')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "点击【添加】按钮",
"locator_type": "ID",
"locator_value": "addButton",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "输入用户名",
"locator_type": "ID",
"locator_value": "username",
"element_type": "input",
"element_value": "test001",
"expected_result": ""
},
{
"page": "system/user",
"step": "点击【确定】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(.,'确定')]",
"element_type": "click",
"element_value": "",
"expected_result": ""
},
{
"page": "system/user",
"step": "获取操作提示",
"locator_type": "XPATH",
"locator_value": "//p[@class='el-message__content']",
"element_type": "getTips",
"element_value": "",
"expected_result": "添加成功"
},
{
"page": "system/user",
"step": "验证列表数据",
"locator_type": "XPATH",
"locator_value": "//td[contains(.,'test001')]",
"element_type": "getText",
"element_value": "",
"expected_result": "列表中包含新添加的用户"
}
],
"platform": "web",
"base_url": "https://192.168.5.44/"
}
```
---
## 8. 风险与应对
| 风险 | 影响 | 概率 | 应对措施 |
|-----|------|------|---------|
| MCP连接不稳定 | 高 | 中 | 添加重试机制和超时处理 |
| 元素定位失败 | 高 | 中 | 提供多种定位方式备选 |
| 页面加载超时 | 中 | 中 | 添加等待和重试逻辑 |
| 登录验证码变化 | 中 | 低 | 支持手动输入或API获取 |
| 菜单结构差异 | 中 | 中 | 支持配置化导航路径 |
| 表单字段识别错误 | 中 | 中 | 添加字段类型识别 |
---
## 9. 验收标准
### 9.1 功能验收
- [ ] 能够正确读取配置文件
- [ ] 能够自动登录系统
- [ ] 能够正确导航到指定模块
- [ ] 能够智能检测元素定位方式
- [ ] 能够执行添加/编辑/删除操作
- [ ] 能够生成符合格式的JSON测试用例
- [ ] 能够批量处理多个模块
### 9.2 代码质量验收
- [ ] 符合代码规范要求(中文注释)
- [ ] 每个模块职责单一
- [ ] 函数有完整的类型注解
- [ ] 异常处理完善
- [ ] 日志记录详细
### 9.3 输出质量验收
- [ ] JSON格式正确
- [ ] 元素定位准确
- [ ] 不使用UID作为定位类型
- [ ] XPATH使用 `contains(.,'text')` 而非 `contains(text(),'text')`
- [ ] 定位器简洁,无多余的多条件组合
- [ ] 操作步骤完整
- [ ] 添加/编辑/删除操作包含getTips验证步骤
- [ ] 添加/编辑/删除操作包含getText列表验证步骤
- [ ] element_value填写正确
- [ ] 只有input类型填写输入内容
- [ ] getTips/getText类型element_value为空
- [ ] click/select类型element_value为空
- [ ] expected_result填写正确
- [ ] getTips类型填写预期提示文本
- [ ] getText类型填写验证描述
- [ ] 文件命名规范
---
## 10. 参考文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
- 需求文档: `Docs/PRD/自动化测试用例生成/_PRD_自动化测试用例生成需求文档.md`
---
## 11. 执行结果记录
### 11.1 代码实现完成(2026-03-06)
**完成内容:**
- ✅ 阶段1:创建项目目录结构
- ✅ 阶段2:实现config_manager模块
- ✅ 阶段3:实现browser_operator模块
- ✅ 阶段4:实现login_handler模块
- ✅ 阶段5:实现navigation_handler模块
- ✅ 阶段6:实现element_locator模块
- ✅ 阶段7:实现operation_executor模块
- ✅ 阶段8:实现testcase_generator模块
- ✅ 阶段9:实现main主控模块
**创建的文件:**
```
AuxiliaryTool/TestCaseGenerator/
├── main.py # 主程序入口(253行)
├── config_manager.py # 配置管理模块(227行)
├── browser_operator.py # 浏览器操作模块(277行)
├── login_handler.py # 登录处理模块(227行)
├── navigation_handler.py # 导航处理模块(221行)
├── element_locator.py # 元素定位模块(326行)
├── operation_executor.py # 操作执行模块(567行)
├── testcase_generator.py # 测试用例生成模块(249行)
├── config/
│ ├── system_config.json # 系统配置示例
│ └── module_config.json # 模块配置示例
├── utils/
│ ├── __init__.py
│ ├── logger.py # 日志工具
│ └── constants.py # 常量定义
├── testcases/ # 输出目录
└── README.md # 使用说明
```
**代码统计:**
- 总代码行数:约2300行
- 模块数量:8个核心模块
- 注释覆盖率:100%(所有函数包含中文注释)
**里程碑达成:**
- ✅ M1: 基础模块完成(config_manager、browser_operator、login_handler)
- ✅ M2: 核心功能完成(navigation、element_locator、operation_executor)
- ✅ M3: 生成功能完成(testcase_generator、main)
**状态:** ⏳ 待集成测试
### 11.2 问题修复记录(2026-03-06)
#### 问题1:XPath定位器文本匹配层级问题
**问题描述**
- `contains(text(), '文本')` 无法匹配Element UI按钮span子元素内的文本
- 定位器存在多条件组合,不够简洁
**修复方案**
| 修复项 | 修复前 | 修复后 |
|--------|--------|--------|
| 文本匹配 | `contains(text(), '确定')` | `contains(., '确定')` |
| 按钮定位 | `//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]` | `//button[contains(., '添加')]` |
| 确定按钮 | `//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定')]` | `//button[contains(., '确定')]` |
| 图标按钮 | `//span[contains(@id, 'edit') and .//i[contains(@class, 'el-icon-edit')]]` | `//i[contains(@class, 'el-icon-edit')]` |
**修复文件**
- `element_locator.py` - XPath生成逻辑
- `区域管理_增值服务_添加.json` - 步骤3、8
- `区域管理_增值服务_编辑.json` - 步骤3、5
- `区域管理_增值服务_删除.json` - 步骤3、4
#### 问题2:element_value 填写规则不统一
**问题描述**
- `getTips``getText` 类型的 `element_value` 填写了验证内容
- 规范要求验证内容应填写在 `expected_result` 字段中
**修复方案**
| element_type | element_value | expected_result |
|-------------|---------------|-----------------|
| `input` | 填写输入内容 | 通常留空 |
| `getTips` | **留空** ✅ | 填写预期提示文本 |
| `getText` | **留空** ✅ | 填写验证描述 |
| `click`/`select` | **留空** ✅ | 通常留空 |
**修复文件**
- `operation_executor.py` - `_get_operation_tips()``_verify_list_data()` 方法
- `区域管理_增值服务_添加.json` - 添加getTips步骤,修正element_value
- `区域管理_增值服务_编辑.json` - 添加getTips步骤,修正element_value
- `区域管理_增值服务_删除.json` - 添加getTips步骤,修正element_value
#### 问题3:新增getText类型区分验证场景
**说明**
- `getTips`:获取弹窗提示信息(如Element UI的el-message组件)
- `getText`:获取列表中的文本数据(如表格td元素)
**代码更新**
- `operation_executor.py` - 新增 `_verify_list_data()` 方法
- 各操作流程中添加列表数据验证步骤
#### 问题4:文档规范同步更新
**更新内容**
1. 需求文档新增3.4节:element_value 和 expected_result 填写规范
2. 需求文档更新示例:XPATH使用 `contains(.,'text')`
3. 计划执行文档新增5.5节:详细填写规范
4. 更新测试用例示例符合新规范
---
## 12. 优化功能回填
*本节将在执行过程中记录优化内容*
---
*文档结束*
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
},
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
\ No newline at end of file
# _PRD_XPATH定位器无法匹配元素_问题记录_计划执行
> 版本:V1.1
> 创建日期:2026-03-06
> 更新日期:2026-03-06
> 适用范围:自动化测试用例生成工具
> 来源:基于《_PRD_运行脚本无法登录系统_问题记录_.md》
> 状态:已完成
---
## 1. 问题概述
### 1.1 问题描述
使用配置文件中的XPATH定位器仍然无法找到登录输入框元素。
### 1.2 问题日志
```
2026-03-06 13:41:51 - LoginHandler - WARNING - 未找到匹配元素: [XPATH] //input[@placeholder='请输入账号或手机号或邮箱号']
2026-03-06 13:41:51 - LoginHandler - ERROR - 使用定位器未找到元素: //input[@placeholder='请输入账号或手机号或邮箱号']
2026-03-06 13:41:51 - LoginHandler - ERROR - 填写用户名失败
2026-03-06 13:41:51 - TestCaseGeneratorMain - ERROR - 执行过程中发生错误: 登录失败
```
---
## 2. 问题分析
### 2.1 根本原因
1. **browser_operator.py 返回空快照**`take_snapshot()` 方法返回 `{"elements": [], "url": "", "title": ""}`
2. **MCP工具未实际调用**:mcp_wrapper.py 中的方法只是模拟实现,没有真正调用 MCP chrome-devtools 工具
3. **XPath匹配逻辑不完整**`_match_xpath_locator` 只实现了简单的 placeholder 和 id 匹配
### 2.2 问题定位
| 文件 | 行号 | 问题 |
|------|------|------|
| browser_operator.py | 71-101 | take_snapshot 返回空字典 |
| mcp_wrapper.py | 68-90 | take_snapshot 返回空元素列表 |
| login_handler.py | 170-199 | XPath匹配逻辑过于简单 |
---
## 3. 解决方案
### 3.1 方案一:使用MCP chrome-devtools工具(推荐)
通过Claude Code的MCP chrome-devtools工具直接操作浏览器,获取元素信息并执行操作。
#### 3.1.1 架构调整
```
TestCaseGeneratorMain
└── BrowserOperator (MCP集成)
├── mcp__chrome-devtools__take_snapshot
├── mcp__chrome-devtools__fill
├── mcp__chrome-devtools__click
└── mcp__chrome-devtools__evaluate_script
```
#### 3.1.2 实现方式
修改 `browser_operator.py``mcp_wrapper.py`,通过Claude Code的Skill工具调用MCP chrome-devtools工具。
### 3.2 方案二:增强本地XPath匹配
如果无法使用MCP工具,需要增强本地的XPath匹配逻辑。
#### 3.2.1 实现完整的XPath解析器
- 支持 `//tag[@attr='value']` 语法
- 支持多种属性匹配
- 支持通配符匹配
---
## 4. 修复执行计划
### Phase 1: 修复MCP工具调用(优先级:P0)✅ 已完成
| 序号 | 任务 | 文件 | 预计工时 | 状态 |
|-----|------|------|----------|------|
| 1 | 修改 browser_operator.py 使用Selenium WebDriver获取真实快照 | browser_operator.py | 30分钟 | ✅ 完成 |
| 2 | 更新 login_handler.py 中的元素匹配逻辑 | login_handler.py | 20分钟 | ✅ 完成 |
### Phase 2: 测试验证(优先级:P0)⏳ 待测试
| 序号 | 任务 | 预计工时 | 状态 |
|-----|------|----------|------|
| 3 | 测试后台登录功能 | 20分钟 | ⏳ 待测试 |
| 4 | 测试前台登录功能 | 20分钟 | ⏳ 待测试 |
---
## 5. 详细修复方案
### 5.1 问题根因
MCP chrome-devtools工具返回的快照格式:
```json
{
"elements": [
{
"uid": "123",
"role": "textbox",
"name": "请输入账号或手机号或邮箱号",
"attributes": {
"placeholder": "请输入账号或手机号或邮箱号",
"type": "text"
}
}
],
"url": "https://192.168.5.44/#/LoginAdmin",
"title": "登录页"
}
```
当前代码问题:
1. `browser_operator.take_snapshot()` 返回空字典
2. `login_handler._find_element_by_locator()` 从空元素列表中查找
3. `_match_xpath_locator()` 匹配逻辑不完整
### 5.2 修复代码
#### 5.2.1 browser_operator.py
需要通过Claude Code MCP工具获取真实快照:
```python
def take_snapshot(self, verbose: bool = False) -> Dict:
"""
获取页面快照(通过MCP chrome-devtools)
"""
self.logger.debug("正在获取页面快照")
try:
# 通过Claude Code的MCP工具获取快照
# 注意:实际执行时需要Claude Code环境支持
from mcp_wrapper import get_mcp_wrapper
mcp = get_mcp_wrapper()
snapshot = mcp.take_snapshot(verbose=verbose)
if not snapshot or "elements" not in snapshot:
self.logger.warning("快照为空,返回默认结构")
return {"elements": [], "url": "", "title": ""}
self.logger.debug(f"页面快照获取成功,共 {len(snapshot.get('elements', []))} 个元素")
return snapshot
except Exception as e:
self.logger.error(f"获取页面快照失败: {e}")
return {"elements": [], "url": "", "title": ""}
```
#### 5.2.2 login_handler.py
增强XPath匹配逻辑,支持从快照中正确提取元素属性:
```python
def _match_xpath_locator(self, element: Dict, xpath: str) -> bool:
"""
增强的XPath匹配(支持placeholder属性)
Args:
element: 元素字典(来自MCP快照)
xpath: XPath表达式
Returns:
是否匹配
"""
import re
# 获取元素属性
element_attrs = element.get("attributes", {})
# 匹配 placeholder 属性
if "@placeholder=" in xpath:
match = re.search(r"@placeholder='([^']+)'", xpath)
if match:
placeholder_value = match.group(1)
element_placeholder = element_attrs.get("placeholder", "")
# 使用in进行部分匹配,允许placeholder包含定位值
return placeholder_value in element_placeholder
# 匹配 id 属性
if "@id=" in xpath:
match = re.search(r"@id='([^']+)'", xpath)
if match:
id_value = match.group(1)
element_id = element_attrs.get("id", "")
return element_id == id_value
# 匹配 name 属性
if "@name=" in xpath:
match = re.search(r"@name='([^']+)'", xpath)
if match:
name_value = match.group(1)
element_name = element_attrs.get("name", "")
return element_name == name_value
# 匹配 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
# 如果没有匹配到任何已知模式,尝试通过元素的name字段匹配
# MCP快照中,输入框的name字段通常显示placeholder文本
element_name = element.get("name", "")
if element_name:
# 提取XPath中的目标值(placeholder后的内容)
placeholder_match = re.search(r"@placeholder='([^']+)'", xpath)
if placeholder_match:
target = placeholder_match.group(1)
return target in element_name
return False
```
### 5.3 备选方案:使用JavaScript直接定位元素
如果XPath匹配仍然有问题,可以使用 `evaluate_script` 直接通过JavaScript查找元素:
```python
def _find_element_by_javascript(self, xpath: str) -> Optional[str]:
"""
使用JavaScript通过XPath查找元素
Args:
xpath: XPath表达式
Returns:
元素的uid(使用DOM路径作为临时uid)
"""
script = f'''
() => {{
const result = document.evaluate("{xpath}", document, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null);
return result.singleNodeValue ? result.singleNodeValue.toString() : null;
}}
'''
# 通过MCP evaluate_script执行
# 这需要实际的MCP工具支持
return None
```
---
## 6. 验收标准
### 6.1 功能验收
- [ ] 能够通过XPATH定位器找到用户名输入框
- [ ] 能够通过XPATH定位器找到密码输入框
- [ ] 能够通过XPATH定位器找到验证码输入框(如果存在)
- [ ] 后台登录能够成功
- [ ] 前台登录能够成功
### 6.2 日志验收
- [ ] 不再出现"未找到匹配元素"警告
- [ ] 正确显示元素定位成功的日志
- [ ] 登录成功后显示"登录成功"日志
---
## 7. 实际修复内容
### 7.1 browser_operator.py 完全重写
使用Selenium WebDriver替代空的MCP包装实现:
- 添加Chrome浏览器初始化(支持无头模式)
- 实现真实的`take_snapshot()`方法,从DOM提取元素信息
- 实现真实的`fill_input()``click_element()`方法
- 实现真实的`execute_script()`方法支持JavaScript执行
- 新增`_extract_element_info()`方法提取元素属性到快照
快照格式(与MCP chrome-devtools兼容):
```python
{
"elements": [
{
"uid": "input-0",
"role": "textbox",
"name": "请输入账号或手机号或邮箱号",
"attributes": {
"id": "...",
"placeholder": "请输入账号或手机号或邮箱号",
"type": "text",
"tag": "input"
}
}
],
"url": "https://192.168.5.44/#/LoginAdmin",
"title": "..."
}
```
### 7.2 login_handler.py 增强XPath匹配
1. **增强`_match_xpath_locator()`方法**
- 支持从`attributes`字典中读取属性值
- 支持placeholder、id、name、type属性匹配
- 添加备选匹配策略(从name字段匹配placeholder)
2. **新增`_fill_input_by_javascript_xpath()`方法**
- 当快照匹配失败时,使用JavaScript直接通过XPath填写输入框
- 支持触发input和change事件
3. **新增`_click_login_button_by_javascript()`方法**
- 当快照中找不到登录按钮时,使用JavaScript直接点击
- 支持多种常见的登录按钮XPath
### 7.3 修复总结
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 快照为空 | browser_operator返回空字典 | 使用Selenium WebDriver获取真实DOM |
| XPath无法匹配 | _match_xpath_locator逻辑太简单 | 增强属性匹配,支持attributes字典 |
| 元素找不到 | 快照属性不完整 | 添加JavaScript直接操作DOM的降级方案 |
---
*文档结束*
# 自动化测试用例生成 - 问题描述文档
## 文档信息
- **创建日期**: 2026-03-06
- **验证轮次**: 第二轮
- **验证范围**: 添加操作测试用例
- **状态**: 待修复
---
## 问题概述
在用户更新了消息定位器后,重新执行添加操作测试用例时,发现了新的关键问题:
1. XPath文本匹配层级问题导致按钮定位失败
2. 确定按钮定位器类名和文本匹配都存在问题
3. 消息元素验证仍然失败(存在时间太短)
---
## 问题清单
### 问题1: 添加按钮XPath定位失败 ⚠️ 严重
**问题描述**
测试用例中的XPath `//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]` 无法定位到添加按钮。
**根因分析**
XPath的`text()`函数只匹配元素的直接文本节点,不包含子元素的文本。Element UI按钮的实际HTML结构为:
```html
<button type="button" class="el-button el-button--primary" id="set_room_list.value_add-create">
<span>添加</span>
</button>
```
"添加"文本在`<span>`子元素内,不是`<button>`的直接文本,因此`contains(text(), '添加')`无法匹配。
**错误定位器**
```xpath
//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]
```
**正确做法**
```xpath
// 使用点号(.)匹配所有后代文本
//button[contains(@class, 'el-button--primary') and contains(., '添加')]
// 或使用 descendant-or-self axis
//button[contains(@class, 'el-button--primary') and .//text()='添加']
// 或直接定位span子元素
//button[contains(@class, 'el-button--primary')]//span[text()='添加']
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤3
- 所有使用`contains(text(), 'xxx')`格式匹配Element UI按钮的场景
**测试结果**
```json
{
"step": 3,
"success": false,
"xpath": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]",
"message": "XPath定位失败 - 未找到添加按钮"
}
```
---
### 问题2: 确定按钮XPath定位失败 ⚠️ 严重
**问题描述**
测试用例中的XPath `//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]` 无法定位到确定按钮。
**根因分析**
存在两个问题:
1. **类名问题**`el-dialog__footer` 类名在Element UI中可能不使用`el-`前缀,实际可能是`dialog-footer`
2. **文本匹配问题**:与问题1相同,`contains(text(), '确定')`无法匹配span内的文本
**错误定位器**
```xpath
//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]
```
**修复建议**
```xpath
// 简化定位器,直接查找确定按钮
//button[contains(@class, 'el-button--primary') and contains(., '确定')]
// 或使用更通用的定位方式
//button[contains(@class, 'el-button--primary')]//span[text()='确定']
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤8
- `区域管理_增值服务_编辑.json` - 步骤5
- 所有对话框确定按钮的定位
**测试结果**
```json
{
"step": 8,
"success": true,
"message": "使用备用定位器成功点击确定按钮",
"note": "原始XPath定位失败"
}
```
---
### 问题3: 消息元素验证失败 ⚠️ 中等
**问题描述**
更新后的定位器 `//p[@class='el-message__content']` 仍然无法捕获到消息元素。
**根因分析**
Element UI的`el-message`组件默认显示时间约为1-2秒,当测试执行到验证步骤时,消息元素已经从DOM中移除。
**当前定位器**
```xpath
//p[@class='el-message__content']
```
**问题表现**
- 操作成功执行(列表数据正确更新)
- 成功消息确实显示过
- 但验证时消息已消失
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤9
- `区域管理_增值服务_编辑.json` - 步骤6
- `区域管理_增值服务_删除.json` - 步骤5
- 所有消息验证步骤
**测试结果**
```json
{
"step": 9,
"success": false,
"xpath": "//p[@class='el-message__content']",
"message": "XPath定位失败 - 未找到提示信息"
}
```
---
## 测试执行详情
### 添加操作测试用例执行记录
| 步骤 | 操作 | 定位器 | 结果 | 备注 |
|------|-----------|--------------|--------|-------|
| 1-2 | 导航到页面 | ID | ✅ 跳过 | 已在页面 |
| 3 | 点击【添加】按钮 | XPATH | ❌ 失败 | 问题1 - 使用UID通过 |
| 4 | 输入商品名称 | XPATH | ✅ 成功 | placeholder定位正常 |
| 5 | 点击商品分类选择框 | XPATH | ✅ 成功 | placeholder定位正常 |
| 6 | 选择分类 | XPATH | ✅ 成功 | 文本定位正常 |
| 7 | 输入商品描述 | XPATH | ✅ 成功 | placeholder定位正常 |
| 8 | 点击【确定】按钮 | XPATH | ⚠️ 备用 | 问题2 - 使用UID通过 |
| 9 | 获取提示文本 | XPATH | ❌ 失败 | 问题3 - 消息已消失 |
**实际操作结果**
- ✅ "测试商品001"成功添加到列表
- ✅ 列表计数从4条变为5条
- ✅ 成功消息"添加成功"确实显示过
- ❌ 验证步骤失败
---
## 技术分析
### XPath文本匹配机制
在XPath中,`text()`的行为:
```javascript
// 只匹配直接文本节点
text() // 元素的直接子文本节点
// 匹配所有文本(包括后代)
. // 当前元素的所有文本内容
text() // 第一个文本节点
//text() // 所有后代文本节点
// 实际示例
<button><span>添加</span></button>
text() = "" // 空,无直接文本
. = "添加" // 包含所有后代文本
.//text() = "添加" // 后代文本
span/text() = "添加" // 子元素文本
```
### Element UI消息组件生命周期
```javascript
// Element UI 消息默认配置
{
duration: 3000, // 显示3秒(但实际可能更短)
showClose: false, // 不显示关闭按钮
center: false, // 不居中
onClose: null // 关闭回调
}
// 消息出现后会在指定时间后自动移除DOM
// 因此测试验证必须在短时间内完成
```
---
## 影响评估
### 严重性评估
| 问题 | 严重性 | 可执行性 | 数据准确性 |
|------|-------|---------|-----------|
| 问题1: 添加按钮定位失败 | 严重 | ❌ 无法执行 | - |
| 问题2: 确定按钮定位失败 | 严重 | ⚠️ 需变通 | ✅ |
| 问题3: 消息验证失败 | 中等 | ✅ 可执行 | ✅ |
### 影响范围统计
- **测试用例文件数量**: 3个(添加、编辑、删除)
- **受影响的步骤**: 约9个步骤
- **必须修复的问题**: 2个(问题1、问题2)
- **可选优化的问题**: 1个(问题3)
---
## 附录
### 相关文档
- 第一轮问题修复文档:`_PRD_执行问题修复.md`
- 测试用例文件:`AuxiliaryTool/TestCaseGenerator/testcases/`
### 验证环境
- 系统地址:https://192.168.5.44
- 测试时间:2026-03-06
- 浏览器:Chrome (via chrome-devtools MCP)
---
**文档结束**
# 自动化测试用例生成 - 问题修复计划执行文档
## 文档信息
- **创建日期**: 2026-03-06
- **版本**: v1.0
- **状态**: 待执行
---
## 修复目标
修复第二轮验证中发现的XPath文本匹配层级问题和消息验证问题,确保测试用例能够稳定执行。
---
## 修复计划清单
### 任务1: 修复按钮定位器的XPath表达式 ✅ 必须修复
**优先级**: P0 - 严重
**预计工时**: 1小时
**问题描述**
`contains(text(), '文本')` 无法匹配Element UI按钮span子元素内的文本
**修复步骤**
1. **修改添加按钮定位器**
```diff
文件: 区域管理_增值服务_添加.json
步骤: 3 - 点击【添加】按钮
- "locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '添加')]"
```
2. **修改确定按钮定位器 - 添加用例**
```diff
文件: 区域管理_增值服务_添加.json
步骤: 8 - 点击【确定】按钮
- "locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '确定')]"
```
3. **修改确定按钮定位器 - 编辑用例**
```diff
文件: 区域管理_增值服务_编辑.json
步骤: 5 - 点击【确定】按钮
- "locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '确定')]"
```
4. **检查其他按钮定位器**
- 搜索所有使用 `contains(text(),` 的XPath
- 确认是否需要修复
**验证方法**
```bash
# 重新执行添加操作测试用例
# 期望:步骤3、8的XPath定位成功
```
**验收标准**
- ✅ 添加按钮定位成功
- ✅ 确定按钮定位成功
- ✅ 无需使用UID等变通方式
---
### 任务2: 优化消息验证方案 ⚠️ 可选优化
**优先级**: P1 - 中等
**预计工时**: 2小时
**问题描述**
el-message元素存在时间太短(1-2秒),XPath验证时已消失
**可选方案**
#### 方案A: 使用数据验证替代消息验证(推荐)
将消息验证改为验证列表数据的变化:
```diff
文件: 区域管理_增值服务_添加.json
步骤: 9 - 验证添加结果
- {
- "step": "获取提示文本",
- "locator_type": "XPATH",
- "locator_value": "//p[@class='el-message__content']",
- "element_type": "getTips",
- "element_value": "添加成功",
- "expected_result": "添加成功"
- }
+ {
+ "step": "验证列表数据",
+ "locator_type": "XPATH",
+ "locator_value": "//td[contains(text(), '测试商品001')]",
+ "element_type": "verify",
+ "element_value": "存在",
+ "expected_result": "列表中包含新添加的商品"
+ }
```
#### 方案B: 在测试执行框架中添加显式等待
```python
def wait_for_message(driver, text, timeout=3):
"""等待消息出现并返回"""
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
try:
element = WebDriverWait(driver, timeout).until(
EC.presence_of_element_located((By.XPATH, f"//p[@class='el-message__content' and contains(text(), '{text}')]"))
)
return element.text
except:
return None
```
#### 方案C: 延长消息显示时间(需要开发支持)
修改Element UI全局配置:
```javascript
// main.js 或相关配置文件
Vue.prototype.$message = Message
Message.defaults.duration = 5000 // 延长到5秒
```
**推荐方案**: 方案A(数据验证)
- 更可靠
- 不依赖消息显示时间
- 直接验证业务结果
**验收标准**
- ✅ 验证步骤能够成功执行
- ✅ 验证结果准确反映操作是否成功
---
## 代码修复清单
### 需要修改的文件
| 文件路径 | 修改内容 | 影响步骤 |
|---------|---------|---------|
| `testcases/区域管理_增值服务_添加.json` | 修复步骤3、8的XPath | 2个步骤 |
| `testcases/区域管理_增值服务_编辑.json` | 修复步骤5的XPath | 1个步骤 |
| `testcases/区域管理_增值服务_删除.json` | 检查确定按钮XPath | 1个步骤 |
### 测试用例生成器修复(可选)
如果要从根本上避免此类问题,需要在测试用例生成器中添加:
```python
# 在生成XPath时,使用 . 而不是 text()
def generate_button_xpath(text, class_name=None):
"""
生成按钮的XPath定位器
Args:
text: 按钮文本
class_name: 按钮类名
"""
if class_name:
# 使用 contains(., 'text') 而不是 contains(text(), 'text')
return f"//button[contains(@class, '{class_name}') and contains(., '{text}')]"
return f"//button[contains(., '{text}')]"
```
---
## 修复执行计划
### Phase 1: 紧急修复(必须) - 预计1小时
1. [ ] 修复添加操作测试用例的XPath(步骤3、8)
2. [ ] 修复编辑操作测试用例的XPath(步骤5)
3. [ ] 检查删除操作测试用例的XPath
4. [ ] 重新执行添加操作测试用例验证
### Phase 2: 验证测试 - 预计30分钟
1. [ ] 完整执行添加操作测试用例
2. [ ] 完整执行编辑操作测试用例
3. [ ] 完整执行删除操作测试用例
4. [ ] 记录测试结果
### Phase 3: 优化改进(可选) - 预计1.5小时
1. [ ] 实施数据验证方案(替代消息验证)
2. [ ] 更新所有三个测试用例的验证步骤
3. [ ] 再次验证所有测试用例
4. [ ] 更新问题文档
---
## XPath修复规范
### 修复前 vs 修复后对比
| 场景 | 错误写法 | 正确写法 |
|------|---------|---------|
| 按钮文本匹配 | `contains(text(), '确定')` | `contains(., '确定')` |
| 按钮精确定位 | `//button[contains(text(), '确定')]` | `//button[contains(., '确定')]` |
| 组合定位 | `//button[contains(text(), '确定') and contains(@class, 'primary')]` | `//button[contains(@class, 'primary') and contains(., '确定')]` |
### 关键规则
1. **匹配包含的文本**:使用 `contains(., 'text')` 而不是 `contains(text(), 'text')`
2. **精确匹配文本**:使用 `.//text()='text'` 而不是 `text()='text'`
3. **简化定位器**:优先使用直接定位而非多层嵌套
---
## 验证计划
### 修复后验证步骤
1. **单步验证**
- 重新执行添加操作测试用例
- 逐步验证每个XPath定位器是否工作
- 记录每步的执行结果
2. **完整验证**
- 连续执行添加→编辑→删除操作
- 验证测试用例的连续性和稳定性
- 确认所有操作都能成功完成
3. **回归验证**
- 检查之前的修复是否仍然有效
- 确保没有引入新的问题
### 成功标准
- ✅ 所有XPath定位器能够正确找到元素
- ✅ 无需使用UID等变通方式
- ✅ 添加、编辑、删除操作都能成功执行
- ✅ 验证步骤能够准确反映操作结果
---
## 风险评估
### 潜在风险
1. **其他按钮定位器可能存在相同问题**
- 风险等级:中
- 缓解措施:全局搜索并检查所有按钮定位器
2. **编辑/删除操作可能遇到类似问题**
- 风险等级:高
- 缓解措施:先验证这两个测试用例
3. **数据验证方案可能不够全面**
- 风险等级:低
- 缓解措施:可以保留消息验证作为可选验证
---
## 附录
### 相关文档
- 问题描述文档:`_PRD_问题描述_问题记录_20260306_第二轮.md`
- 第一轮修复文档:`_PRD_执行问题修复.md`
### 修复历史
| 日期 | 轮次 | 修复内容 | 状态 |
|------|------|---------|------|
| 2026-03-06 | 第一轮 | ID重复、XPath索引、消息定位 | 已完成 |
| 2026-03-06 | 第二轮 | XPath文本匹配层级 | 待执行 |
---
**文档结束**
# 自动化测试用例生成 - 执行问题修复文档
## 文档信息
- **创建日期**: 2026-03-06
- **版本**: v1.1
- **最后更新**:
- **状态**:
---
## 问题概述
通过main.py运行时,日志打印未找到元素用户、账号、username、account、用户,登录失败。
---
## 发现的问题清单
### 问题1: 无法登录系统
**问题描述**
- 登录失败,未找到用户名输入框
**问题日志**
- 2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: 用户名
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: 账号
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: username
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: account
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: 用户
2026-03-06 13:25:37 - LoginHandler - ERROR - 未找到用户名输入框
2026-03-06 13:25:37 - TestCaseGeneratorMain - ERROR - 执行过程中发生错误: 登录失败
2026-03-06 13:25:37 - TestCaseGeneratorMain - INFO - 正在清理资源...
2026-03-06 13:25:37 - BrowserOperator - INFO - 正在关闭浏览器
2026-03-06 13:25:37 - BrowserOperator - INFO - 浏览器已关闭
2026-03-06 13:25:37 - TestCaseGeneratorMain - INFO - 资源清理完成
2026-03-06 13:41:51 - LoginHandler - WARNING - 未找到匹配元素: [XPATH] //input[@placeholder='请输入账号或手机号或邮箱号']
2026-03-06 13:41:51 - LoginHandler - ERROR - 使用定位器未找到元素: //input[@placeholder='请输入账号或手机号或邮箱号']
2026-03-06 13:41:51 - LoginHandler - ERROR - 填写用户名失败
2026-03-06 13:41:51 - TestCaseGeneratorMain - ERROR - 执行过程中发生错误: 登录失败
2026-03-06 13:41:51 - TestCaseGeneratorMain - INFO - 正在清理资源...
2026-03-06 13:41:51 - BrowserOperator - INFO - 正在关闭浏览器
2026-03-06 13:41:51 - BrowserOperator - INFO - 浏览器已关闭
2026-03-06 13:41:51 - TestCaseGeneratorMain - INFO - 资源清理完成
错误: 登录失败
---
**文档结束**
# _PRD_运行脚本无法登录系统_问题记录_计划执行
> 版本:V2.0
> 创建日期:2026-03-06
> 更新日期:2026-03-06
> 适用范围:自动化测试用例生成工具
> 来源:基于《_PRD_运行脚本无法登录系统_问题记录_.md》
> 状态:待执行
---
## 1. 问题概述
### 1.1 问题描述
运行 main.py 时,登录失败,日志显示未找到用户名输入框。
### 1.2 问题日志
```
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: 用户名
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: 账号
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: username
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: account
2026-03-06 13:25:37 - BrowserOperator - WARNING - 未找到元素: 用户
2026-03-06 13:25:37 - LoginHandler - ERROR - 未找到用户名输入框
2026-03-06 13:25:37 - TestCaseGeneratorMain - ERROR - 执行过程中发生错误: 登录失败
```
---
## 2. 解决方案
### 2.1 新方案:配置文件指定登录定位器
**设计变更**:不再自动查找输入框,而是在 `system_config.json` 中直接指定登录元素的定位器。
#### 2.1.1 新的 system_config.json 格式
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"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"]
}
]
```
#### 2.1.2 定位器配置格式
**格式**`["定位类型", "定位值", "输入值"]`
| 字段 | 说明 | 示例 |
|------|------|------|
| 定位类型 | XPATH/ID/CSS_SELECTOR等 | `"XPATH"` |
| 定位值 | 元素定位表达式 | `"//input[@placeholder='请输入账号']"` |
| 输入值 | 要填写的值 | `"admin@xty"` |
---
## 3. 修复执行计划
### Phase 1: 更新配置文件(优先级:P0)
| 序号 | 任务 | 文件 | 预计工时 |
|-----|------|------|----------|
| 1 | 更新 system_config.json 格式 | config/system_config.json | 10分钟 |
### Phase 2: 更新代码实现(优先级:P0)
| 序号 | 任务 | 文件 | 预计工时 |
|-----|------|------|----------|
| 2 | 更新 config_manager.py 解析新格式 | config_manager.py | 30分钟 |
| 3 | 重写 login_handler.py 使用配置定位器 | login_handler.py | 30分钟 |
| 4 | 更新 main.py 传递登录类型参数 | main.py | 15分钟 |
### Phase 3: 测试验证(优先级:P1)
| 序号 | 任务 | 预计工时 |
|-----|------|----------|
| 5 | 测试后台登录 | 15分钟 |
| 6 | 测试前台登录 | 15分钟 |
---
## 4. 详细修复代码
### 4.1 system_config.json
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"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"]
}
]
```
### 4.2 config_manager.py 更新
```python
def _validate_system_config(self, config: List[Dict]) -> bool:
"""验证系统配置格式(更新版)"""
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
# 检查登录字段格式
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
return True
```
### 4.3 login_handler.py 更新
```python
def login(self, url: str, login_type: str = "back") -> bool:
"""
执行登录操作(使用配置的定位器)
Args:
url: 登录页面URL
login_type: 登录类型,"back"或"front"
Returns:
是否登录成功
"""
# 从配置中获取登录信息
system_config = self.config_manager.get_system_config()
# 根据登录类型选择配置
username_config = system_config.get(f"{login_type}_username")
password_config = system_config.get(f"{login_type}_password")
code_config = system_config.get(f"{login_type}_code")
# 使用配置的定位器进行登录
locator_type, locator_value, username_value = username_config
# ... 填写用户名
locator_type, locator_value, password_value = password_config
# ... 填写密码
# ...
```
---
## 5. 验收标准
### 5.1 功能验收
- [ ] 能够成功解析新的配置格式
- [ ] 能够使用配置的定位器找到输入框
- [ ] 后台登录能够成功
- [ ] 前台登录能够成功
### 5.2 配置验收
- [ ] system_config.json 格式正确
- [ ] 所有登录字段配置完整
- [ ] 定位器能够正确定位元素
---
*文档结束*
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论