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

feat(test-case-generator): 重构浏览器操作模块并实现配置驱动的登录功能

- 将BrowserOperator从MCP工具切换到Selenium WebDriver实现
- 添加Chrome浏览器初始化和元素定位功能
- 实现基于配置文件的登录参数验证和处理
- 移除重复的系统配置项并更新验证逻辑
- 添加问题修复文档记录定位器相关的执行问题
上级 4faab7e2
# -*- coding: utf-8 -*-
"""
浏览器操作模块
封装chrome-devtools MCP工具,提供统一的浏览器操作接口
使用Selenium WebDriver提供浏览器操作接口
"""
import time
import uuid
from typing import Dict, List, Optional, Any
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.remote.webelement import WebElement
from webdriver_manager.chrome import ChromeDriverManager
from utils.logger import get_logger
from utils.constants import WAIT_TIMEOUT, WAIT_SHORT, MCP_TIMEOUT
class BrowserOperator:
"""浏览器操作器 - 封装chrome-devtools MCP工具"""
"""浏览器操作器 - 使用Selenium WebDriver"""
def __init__(self, headless: bool = False):
"""
初始化浏览器操作器
def __init__(self):
"""初始化浏览器操作器"""
Args:
headless: 是否使用无头模式
"""
self.logger = get_logger("BrowserOperator")
self.driver: Optional[webdriver.Chrome] = None
self.current_url: Optional[str] = None
self.page_id: Optional[int] = None
self.is_connected = False
self._element_counter = 0
self._uid_to_element: Dict[str, WebElement] = {}
# 初始化浏览器
self._init_browser(headless)
def _init_browser(self, headless: bool) -> None:
"""初始化Chrome浏览器"""
try:
chrome_options = Options()
if headless:
chrome_options.add_argument("--headless")
# 常用Chrome选项
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("--lang=zh-CN")
# 忽略SSL错误
chrome_options.add_argument("--ignore-certificate-errors")
chrome_options.add_argument("--allow-running-insecure-content")
# 自动安装ChromeDriver
service = Service(ChromeDriverManager().install())
self.driver = webdriver.Chrome(service=service, options=chrome_options)
self.driver.implicitly_wait(10)
self.is_connected = True
self.logger.info("浏览器初始化成功")
except Exception as e:
self.logger.error(f"浏览器初始化失败: {e}")
raise
def open_page(self, url: str, timeout: int = MCP_TIMEOUT) -> bool:
"""
......@@ -30,17 +80,12 @@ class BrowserOperator:
Returns:
是否成功打开页面
Note:
此方法使用mcp__chrome-devtools__new_page工具
"""
self.logger.info(f"正在打开页面: {url}")
try:
# 注意:实际调用时需要使用MCP工具
# 这里是接口定义,实际实现在主程序中通过MCP调用
self.current_url = url
self.is_connected = True
self.driver.get(url)
self.current_url = self.driver.current_url
self.logger.info(f"页面打开成功: {url}")
return True
except Exception as e:
......@@ -61,7 +106,10 @@ class BrowserOperator:
self.logger.info(f"正在导航到: {url}")
try:
self.current_url = url
if ignore_cache:
self.driver.delete_all_cookies()
self.driver.get(url)
self.current_url = self.driver.current_url
self.logger.info(f"导航成功: {url}")
return True
except Exception as e:
......@@ -70,43 +118,126 @@ class BrowserOperator:
def take_snapshot(self, verbose: bool = False) -> Dict:
"""
获取页面快照(可访问性树
获取页面快照(模拟MCP格式
Args:
verbose: 是否包含详细信息
Returns:
页面快照字典,包含元素uid和相关信息
Note:
返回格式示例:
{
"elements": [
{
"uid": "123",
"role": "button",
"name": "确定",
"attributes": {...}
},
...
]
}
"""
self.logger.debug("正在获取页面快照")
try:
# 返回快照数据
snapshot = {}
self.logger.debug("页面快照获取成功")
elements = []
self._uid_to_element.clear()
# 获取所有输入框
try:
inputs = self.driver.find_elements(By.XPATH, "//input")
for idx, elem in enumerate(inputs):
uid = f"input-{idx}"
self._uid_to_element[uid] = elem
element_info = self._extract_element_info(elem, uid, "textbox")
elements.append(element_info)
except Exception as e:
self.logger.debug(f"获取输入框时出错: {e}")
# 获取所有按钮
try:
buttons = self.driver.find_elements(By.XPATH, "//button")
for idx, elem in enumerate(buttons):
uid = f"button-{idx}"
self._uid_to_element[uid] = elem
element_info = self._extract_element_info(elem, uid, "button")
elements.append(element_info)
except Exception as e:
self.logger.debug(f"获取按钮时出错: {e}")
# 获取所有文本区域
try:
textareas = self.driver.find_elements(By.XPATH, "//textarea")
for idx, elem in enumerate(textareas):
uid = f"textarea-{idx}"
self._uid_to_element[uid] = elem
element_info = self._extract_element_info(elem, uid, "textbox")
elements.append(element_info)
except Exception as e:
self.logger.debug(f"获取文本区域时出错: {e}")
snapshot = {
"elements": elements,
"url": self.driver.current_url if self.driver else "",
"title": self.driver.title if self.driver else ""
}
self.logger.debug(f"页面快照获取成功,共 {len(elements)} 个元素")
return snapshot
except Exception as e:
self.logger.error(f"获取页面快照失败: {e}")
return {}
return {"elements": [], "url": "", "title": ""}
def _extract_element_info(self, element: WebElement, uid: str, default_role: str) -> Dict:
"""
提取元素信息
Args:
element: Selenium WebElement
uid: 元素唯一标识
default_role: 默认角色
Returns:
元素信息字典
"""
info = {
"uid": uid,
"role": default_role,
"name": "",
"attributes": {}
}
try:
# 获取文本内容
try:
text = element.text
if text:
info["name"] = text
except:
pass
# 获取属性
attributes = ["id", "name", "type", "class", "placeholder", "value", "role", "aria-label"]
for attr in attributes:
try:
value = element.get_attribute(attr)
if value:
info["attributes"][attr] = value
# 如果name为空,使用placeholder作为name
if not info["name"] and attr == "placeholder":
info["name"] = value
except:
pass
# 获取标签名
try:
info["attributes"]["tag"] = element.tag_name
except:
pass
except Exception as e:
self.logger.debug(f"提取元素信息时出错: {e}")
return info
def find_element_by_text(self, snapshot: Dict, text: str, exact_match: bool = False) -> Optional[str]:
"""
在页面快照中查找包含指定文本的元素
搜索范围:
1. 元素的 name 字段(可访问性名称)
2. 元素的 placeholder 属性
Args:
snapshot: 页面快照
text: 要查找的文本
......@@ -121,25 +252,103 @@ class BrowserOperator:
elements = snapshot.get("elements", [])
for element in elements:
# 策略1: 搜索 name 字段
element_text = element.get("name", "")
if exact_match:
if element_text == text:
uid = element.get("uid")
self.logger.debug(f"找到元素: {text}, uid: {uid}")
self.logger.debug(f"找到元素(name匹配): {text}, uid: {uid}")
return uid
else:
if text in element_text:
uid = element.get("uid")
self.logger.debug(f"找到元素: {text}, uid: {uid}")
self.logger.debug(f"找到元素(name匹配): {text}, uid: {uid}")
return uid
# 策略2: 搜索 placeholder 属性
attrs = element.get("attributes", {})
placeholder = attrs.get("placeholder", "")
if placeholder:
if exact_match:
if placeholder == text:
uid = element.get("uid")
self.logger.debug(f"找到元素(placeholder匹配): {text}, uid: {uid}")
return uid
else:
if text in placeholder:
uid = element.get("uid")
self.logger.debug(f"找到元素(placeholder匹配): {text}, uid: {uid}")
return uid
self.logger.warning(f"未找到元素: {text}")
return None
except Exception as e:
self.logger.error(f"查找元素失败: {e}")
return None
def find_element_by_attributes(self, snapshot: Dict, **attributes) -> Optional[str]:
"""
通过属性查找元素
Args:
snapshot: 页面快照
**attributes: 要匹配的属性键值对,如 placeholder="用户名"
Returns:
元素的uid,如果未找到则返回None
"""
try:
elements = snapshot.get("elements", [])
for element in elements:
match = True
for key, value in attributes.items():
attrs = element.get("attributes", {})
element_value = str(attrs.get(key, ""))
if value not in element_value:
match = False
break
if match:
uid = element.get("uid")
self.logger.debug(f"找到匹配元素(属性: {attributes}), uid: {uid}")
return uid
self.logger.warning(f"未找到匹配元素的属性: {attributes}")
return None
except Exception as e:
self.logger.error(f"通过属性查找元素失败: {e}")
return None
def find_inputs_by_role(self, snapshot: Dict, role: str = "textbox") -> List[Dict]:
"""
查找所有指定角色的输入元素
Args:
snapshot: 页面快照
role: 角色,默认为textbox
Returns:
匹配的元素列表,包含uid和属性信息
"""
try:
elements = snapshot.get("elements", [])
matched_elements = []
for element in elements:
element_role = element.get("role", "")
if element_role == role:
matched_elements.append({
"uid": element.get("uid"),
"attributes": element.get("attributes", {})
})
self.logger.debug(f"找到 {len(matched_elements)} 个 role='{role}' 的元素")
return matched_elements
except Exception as e:
self.logger.error(f"查找输入元素失败: {e}")
return []
def click_element(self, uid: str, wait_after: int = WAIT_SHORT) -> bool:
"""
点击指定元素
......@@ -154,6 +363,13 @@ class BrowserOperator:
self.logger.debug(f"正在点击元素: {uid}")
try:
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return False
element.click()
# 等待页面响应
if wait_after > 0:
time.sleep(wait_after)
......@@ -179,6 +395,15 @@ class BrowserOperator:
self.logger.debug(f"正在填写输入框: {uid}, 值: {value}")
try:
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return False
if clear_first:
element.clear()
element.send_keys(value)
self.logger.debug(f"输入框填写成功: {uid}")
return True
except Exception as e:
......@@ -199,6 +424,15 @@ class BrowserOperator:
self.logger.debug(f"正在选择下拉选项: {uid}, 值: {value}")
try:
from selenium.webdriver.support.select import Select
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return False
select = Select(element)
select.select_by_visible_text(value)
self.logger.debug(f"下拉选项选择成功: {uid}")
return True
except Exception as e:
......@@ -212,6 +446,11 @@ class BrowserOperator:
Returns:
当前页面URL
"""
try:
if self.driver:
return self.driver.current_url
except:
pass
return self.current_url
def wait_for_element(self, text: str, timeout: int = WAIT_TIMEOUT) -> bool:
......@@ -282,9 +521,11 @@ class BrowserOperator:
self.logger.debug(f"正在执行脚本: {script[:50]}...")
try:
result = None
self.logger.debug("脚本执行成功")
return result
if self.driver:
result = self.driver.execute_script(script)
self.logger.debug("脚本执行成功")
return result
return None
except Exception as e:
self.logger.error(f"脚本执行失败: {e}")
return None
......@@ -302,9 +543,23 @@ class BrowserOperator:
self.logger.debug(f"正在获取元素属性: {uid}")
try:
attributes = {}
element = self._uid_to_element.get(uid)
if not element:
self.logger.error(f"未找到元素: {uid}")
return {}
# 获取所有属性
attrs = {}
for attr in ["id", "name", "class", "type", "placeholder", "value", "role", "aria-label"]:
try:
value = element.get_attribute(attr)
if value:
attrs[attr] = value
except:
pass
self.logger.debug(f"元素属性获取成功: {uid}")
return attributes
return attrs
except Exception as e:
self.logger.error(f"获取元素属性失败: {e}")
return {}
......@@ -314,8 +569,12 @@ class BrowserOperator:
self.logger.info("正在关闭浏览器")
try:
if self.driver:
self.driver.quit()
self.driver = None
self.is_connected = False
self.current_url = None
self._uid_to_element.clear()
self.logger.info("浏览器已关闭")
except Exception as e:
self.logger.error(f"关闭浏览器失败: {e}")
......@@ -3,8 +3,11 @@
"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"
"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"]
}
]
......@@ -114,10 +114,10 @@ class ConfigManager:
def _validate_system_config(self, config: List[Dict]) -> bool:
"""
验证系统配置格式
验证系统配置格式(更新版)
Args:
config: 系统配置字典
config: 系统配置字典列表
Returns:
验证是否通过
......@@ -126,14 +126,12 @@ class ConfigManager:
self.logger.error("系统配置必须是列表格式")
return False
required_fields = [
"system_type",
"system_front_url",
"system_back_url",
"username",
"password",
"code"
]
# 基础必填字段
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):
......@@ -155,8 +153,61 @@ class ConfigManager:
self.logger.error(f"系统配置第{idx+1}项的system_back_url格式错误")
return False
# 检查登录字段格式(如果存在)
for field in login_fields:
if field in system:
field_value = system[field]
if not isinstance(field_value, list) or len(field_value) != 3:
self.logger.error(f"登录字段 {field} 格式错误,应为 [定位类型, 定位值, 输入值]")
return False
# 验证定位类型
locator_type = field_value[0]
if locator_type not in ["ID", "XPATH", "CSS_SELECTOR", "NAME"]:
self.logger.error(f"不支持的定位类型: {locator_type}")
return False
return True
def get_login_config(self, login_type: str = "back") -> Dict[str, tuple]:
"""
获取登录配置
Args:
login_type: 登录类型,"back"或"front"
Returns:
登录配置字典,格式:
{
"username": ("XPATH", "//input[@placeholder='...']", "admin@xty"),
"password": ("XPATH", "//input[@placeholder='...']", "password"),
"code": ("XPATH", "//input[@placeholder='...']", "csba")
}
Raises:
ValueError: 配置未加载或不支持的登录类型
"""
if self.system_config is None:
raise ValueError("系统配置未加载,请先调用load_system_config()")
system = self.system_config[0] # 使用第一个系统配置
# 根据登录类型获取配置
prefix = f"{login_type}_"
login_config = {}
for field in ["username", "password", "code"]:
config_key = f"{prefix}{field}"
if config_key not in system:
self.logger.error(f"缺少登录配置: {config_key}")
raise ValueError(f"缺少登录配置: {config_key}")
config_value = system[config_key]
# 格式:["定位类型", "定位值", "输入值"]
login_config[field] = (config_value[0], config_value[1], config_value[2])
return login_config
def _validate_module_config(self, config: List[Dict]) -> bool:
"""
验证模块配置格式
......
# -*- coding: utf-8 -*-
"""
登录处理模块
负责处理系统登录流程
负责处理系统登录流程(使用配置文件中的定位器)
"""
from typing import Dict, Optional
from typing import Dict, Optional, List, Tuple
from browser_operator import BrowserOperator
from utils.logger import get_logger
from utils.constants import WAIT_TIMEOUT, WAIT_SHORT
from config_manager import ConfigManager
class LoginHandler:
"""登录处理器"""
def __init__(self, browser: BrowserOperator):
def __init__(self, browser: BrowserOperator, config_manager: ConfigManager):
"""
初始化登录处理器
Args:
browser: 浏览器操作器实例
config_manager: 配置管理器实例
"""
self.browser = browser
self.config_manager = config_manager
self.logger = get_logger("LoginHandler")
def login(self, url: str, username: str, password: str, code: str = "csba") -> bool:
def login(self, url: str, login_type: str = "back") -> bool:
"""
执行登录操作
执行登录操作(使用配置文件中的定位器)
Args:
url: 登录页面URL
username: 用户名
password: 密码
code: 验证码,默认为csba
login_type: 登录类型,"back"或"front"
Returns:
是否登录成功
"""
self.logger.info(f"开始登录流程,用户: {username}")
self.logger.info(f"开始登录流程,登录类型: {login_type}")
# 1. 打开登录页面
# 1. 获取登录配置
try:
login_config = self.config_manager.get_login_config(login_type)
except ValueError as e:
self.logger.error(f"获取登录配置失败: {e}")
return False
username_config = login_config["username"]
password_config = login_config["password"]
code_config = login_config["code"]
# 2. 打开登录页面
self.logger.info("正在打开登录页面...")
if not self.browser.open_page(url):
self.logger.error("打开登录页面失败")
......@@ -48,56 +60,44 @@ class LoginHandler:
import time
time.sleep(WAIT_SHORT)
# 2. 获取页面快照
# 3. 获取页面快照
snapshot = self.browser.take_snapshot()
# 3. 查找并填写用户名
username_uid = self._find_username_input(snapshot)
if not username_uid:
self.logger.error("未找到用户名输入框")
return False
self.logger.info(f"正在填写用户名: {username}")
if not self.browser.fill_input(username_uid, username):
# 4. 使用配置的定位器填写用户名
if not self._fill_input_by_locator(snapshot, username_config):
self.logger.error("填写用户名失败")
return False
# 4. 查找并填写密码
password_uid = self._find_password_input(snapshot)
if not password_uid:
self.logger.error("未找到密码输入框")
return False
self.logger.info("正在填写密码")
if not self.browser.fill_input(password_uid, password):
# 5. 使用配置的定位器填写密码
if not self._fill_input_by_locator(snapshot, password_config):
self.logger.error("填写密码失败")
return False
# 5. 查找并填写验证码
code_uid = self._find_code_input(snapshot)
if code_uid:
self.logger.info(f"正在填写验证码: {code}")
if not self.browser.fill_input(code_uid, code):
# 6. 使用配置的定位器填写验证码(如果存在)
if code_config:
if not self._fill_input_by_locator(snapshot, code_config):
self.logger.warning("填写验证码失败,继续尝试登录")
else:
self.logger.warning("未找到验证码输入框,可能不需要验证码")
self.logger.info("无验证码配置,跳过验证码填写")
# 6. 查找并点击登录按钮
# 7. 查找并点击登录按钮
login_button_uid = self._find_login_button(snapshot)
if not login_button_uid:
self.logger.error("未找到登录按钮")
return False
self.logger.info("正在点击登录按钮")
if not self.browser.click_element(login_button_uid):
self.logger.error("点击登录按钮失败")
return False
self.logger.warning("快照中未找到登录按钮,尝试使用JavaScript直接查找并点击")
if not self._click_login_button_by_javascript():
self.logger.error("未找到登录按钮")
return False
else:
self.logger.info("正在点击登录按钮")
if not self.browser.click_element(login_button_uid):
self.logger.error("点击登录按钮失败")
return False
# 7. 等待登录完成
# 8. 等待登录完成
self.logger.info("等待登录完成...")
time.sleep(WAIT_SHORT)
# 8. 验证登录成功
# 9. 验证登录成功
if self.verify_login_success():
self.logger.info("登录成功")
return True
......@@ -105,69 +105,218 @@ class LoginHandler:
self.logger.error("登录验证失败")
return False
def _find_username_input(self, snapshot: Dict) -> Optional[str]:
def _fill_input_by_locator(self, snapshot: Dict, config: Tuple) -> bool:
"""
查找用户名输入框
使用配置的定位器填写输入框
Args:
snapshot: 页面快照
config: 配置元组 (locator_type, locator_value, input_value)
Returns:
用户名输入框的uid,未找到返回None
是否成功填写
"""
# 常见的用户名输入框标识
username_keywords = ["用户名", "账号", "username", "account", "用户"]
locator_type, locator_value, input_value = config
for keyword in username_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
return uid
self.logger.debug(f"使用定位器填写输入: [{locator_type}, {locator_value}, {input_value}]")
return None
# 查找元素
uid = self._find_element_by_locator(snapshot, locator_type, locator_value)
if not uid:
# 对于XPATH定位器,尝试使用JavaScript直接查找并填写
if locator_type == "XPATH":
self.logger.warning(f"快照中未找到元素,尝试使用JavaScript直接查找: {locator_value}")
return self._fill_input_by_javascript_xpath(locator_value, input_value)
self.logger.error(f"使用定位器未找到元素: {locator_value}")
return False
def _find_password_input(self, snapshot: Dict) -> Optional[str]:
# 填写输入框
self.logger.info(f"正在填写值: {input_value}")
if not self.browser.fill_input(uid, input_value):
self.logger.error("填写失败")
return False
return True
def _fill_input_by_javascript_xpath(self, xpath: str, value: str) -> bool:
"""
查找密码输入框
使用JavaScript通过XPath直接填写输入框
Args:
snapshot: 页面快照
xpath: XPath表达式
value: 要填写的值
Returns:
密码输入框的uid,未找到返回None
是否成功填写
"""
# 常见的密码输入框标识
password_keywords = ["密码", "password", "pwd"]
script = f'''
() => {{
const result = document.evaluate("{xpath}", document, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const element = result.singleNodeValue;
if (element) {{
element.value = "{value}";
element.dispatchEvent(new Event('input', {{ bubbles: true }}));
element.dispatchEvent(new Event('change', {{ bubbles: true }}));
return true;
}}
return false;
}}
'''
for keyword in password_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
return uid
try:
result = self.browser.execute_script(script)
if result:
self.logger.info(f"使用JavaScript填写成功: {xpath}")
return True
else:
self.logger.error(f"JavaScript填写失败,未找到元素: {xpath}")
return False
except Exception as e:
self.logger.error(f"JavaScript填写异常: {e}")
return False
def _find_element_by_locator(self, snapshot: Dict, locator_type: str, locator_value: str) -> Optional[str]:
"""
根据定位器类型和值查找元素
Args:
snapshot: 页面快照
locator_type: 定位类型(XPATH/ID/CSS_SELECTOR等)
locator_value: 定位值
Returns:
元素的uid,未找到返回None
"""
elements = snapshot.get("elements", [])
for element in elements:
if locator_type == "XPATH":
# XPath定位需要通过JavaScript或其他方式匹配
# 这里简化处理:如果元素的某个属性包含定位值的部分内容
if self._match_xpath_locator(element, locator_value):
return element.get("uid")
elif locator_type == "ID":
if element.get("id") == locator_value:
return element.get("uid")
elif locator_type == "NAME":
if element.get("name") == locator_value:
return element.get("uid")
elif locator_type == "CSS_SELECTOR":
# CSS选择器匹配
if self._match_css_selector(element, locator_value):
return element.get("uid")
self.logger.warning(f"未找到匹配元素: [{locator_type}] {locator_value}")
return None
def _find_code_input(self, snapshot: Dict) -> Optional[str]:
def _match_xpath_locator(self, element: Dict, xpath: str) -> bool:
"""
查找验证码输入框
增强的XPath匹配(支持MCP快照格式)
支持的XPath格式:
- //input[@placeholder='value']
- //input[@id='value']
- //input[@name='value']
- //input[@type='value']
Args:
snapshot: 页面快照
element: 元素字典(来自MCP快照)
xpath: XPath表达式
Returns:
验证码输入框的uid,未找到返回None
是否匹配
"""
# 常见的验证码输入框标识
code_keywords = ["验证码", "code", "captcha"]
import re
# MCP快照中的元素格式:
# {
# "uid": "...",
# "role": "textbox",
# "name": "显示文本(可能是placeholder)",
# "attributes": {"placeholder": "...", "type": "...", "id": "...", ...}
# }
# 获取元素属性(MCP快照将属性存在attributes字段中)
element_attrs = element.get("attributes", {})
element_name = element.get("name", "")
# 匹配 placeholder 属性
if "@placeholder=" in xpath:
match = re.search(r"@placeholder='([^']+)'", xpath)
if match:
placeholder_value = match.group(1)
# 优先从attributes中获取placeholder
element_placeholder = element_attrs.get("placeholder", "")
if element_placeholder:
return placeholder_value == element_placeholder or placeholder_value in element_placeholder
# 备选:从name字段匹配(MCP快照中name可能显示placeholder文本)
if placeholder_value in element_name:
return True
# 匹配 id 属性
if "@id=" in xpath:
match = re.search(r"@id='([^']+)'", xpath)
if match:
id_value = match.group(1)
# 优先从attributes中获取id
element_id = element_attrs.get("id", "")
if element_id:
return element_id == id_value
# 备选:从顶层id字段获取
if element.get("id") == id_value:
return True
# 匹配 name 属性
if "@name=" in xpath:
match = re.search(r"@name='([^']+)'", xpath)
if match:
name_value = match.group(1)
# 优先从attributes中获取name
element_attr_name = element_attrs.get("name", "")
if element_attr_name:
return element_attr_name == name_value
# 备选:从顶层name字段获取
if element.get("name") == name_value:
return True
# 匹配 type 属性
if "@type=" in xpath:
match = re.search(r"@type='([^']+)'", xpath)
if match:
type_value = match.group(1)
element_type = element_attrs.get("type", "")
return element_type == type_value
for keyword in code_keywords:
uid = self.browser.find_element_by_text(snapshot, keyword)
if uid:
return uid
return False
return None
def _match_css_selector(self, element: Dict, selector: str) -> bool:
"""
简化的CSS选择器匹配
Args:
element: 元素字典
selector: CSS选择器
Returns:
是否匹配
"""
# 简化实现:支持class选择器
if selector.startswith(".") and "class" in element:
class_value = selector[1:]
element_classes = element.get("class", "")
if isinstance(element_classes, str):
return class_value in element_classes.split()
elif isinstance(element_classes, list):
return class_value in element_classes
return False
def _find_login_button(self, snapshot: Dict) -> Optional[str]:
"""
查找登录按钮
查找登录按钮(增强版)
Args:
snapshot: 页面快照
......@@ -183,8 +332,61 @@ class LoginHandler:
if uid:
return uid
# 尝试通过XPATH查找
elements = snapshot.get("elements", [])
for element in elements:
role = element.get("role", "")
name = element.get("name", "")
if (role == "button" or element.get("tag") == "button") and \
any(keyword in name for keyword in ["登录", "提交"]):
return element.get("uid")
self.logger.warning("未找到登录按钮")
return None
# 旧的自动发现方法已移除,改用配置文件指定的定位器
def _click_login_button_by_javascript(self) -> bool:
"""
使用JavaScript查找并点击登录按钮
Returns:
是否成功点击
"""
# 常见的登录按钮XPATH
login_xpaths = [
"//button[contains(.,'登录')]",
"//button[contains(.,'登 录')]",
"//button[contains(.,'提交')]",
"//input[@type='submit']",
"//button[@type='submit']",
]
for xpath in login_xpaths:
script = f'''
() => {{
const result = document.evaluate("{xpath}", document, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const element = result.singleNodeValue;
if (element) {{
element.click();
return true;
}}
return false;
}}
'''
try:
result = self.browser.execute_script(script)
if result:
self.logger.info(f"使用JavaScript点击登录按钮成功: {xpath}")
return True
except Exception as e:
self.logger.debug(f"JavaScript点击失败: {xpath}, 错误: {e}")
self.logger.error("JavaScript未找到登录按钮")
return False
def verify_login_success(self) -> bool:
"""
验证登录是否成功
......
......@@ -27,13 +27,15 @@ class TestCaseGeneratorMain:
"""测试用例生成器主控类"""
def __init__(self, system_config_path: str = "config/system_config.json",
module_config_path: str = "config/module_config.json"):
module_config_path: str = "config/module_config.json",
login_type: str = "back"):
"""
初始化主控类
Args:
system_config_path: 系统配置文件路径
module_config_path: 模块配置文件路径
login_type: 登录类型,"front"或"back",默认为"back"
"""
self.logger = get_logger("TestCaseGeneratorMain")
self.config_manager = ConfigManager()
......@@ -46,6 +48,7 @@ class TestCaseGeneratorMain:
# 配置文件路径
self.system_config_path = system_config_path
self.module_config_path = module_config_path
self.login_type = login_type
# 设置文件日志
project_root = Path(__file__).parent
......@@ -62,15 +65,15 @@ class TestCaseGeneratorMain:
# 1. 加载配置
self._load_configs()
# 2. 选择登录URL
# 2. 选择登录URL和登录类型
system_config = self.config_manager.get_system_config()
login_url = self._select_login_url(system_config)
login_url, login_type = self._select_login_url_and_type(system_config, self.login_type)
# 3. 初始化浏览器
self._initialize_browser()
# 4. 登录系统
self._login_system(login_url, system_config)
self._login_system(login_url, login_type)
# 5. 处理所有模块
self._process_all_modules()
......@@ -110,26 +113,25 @@ class TestCaseGeneratorMain:
functions = module.get("module_function")
self.logger.info(f" 模块{idx}: {module_name} > {module_name_son}, 功能: {functions}")
def _select_login_url(self, system_config: Dict) -> str:
def _select_login_url_and_type(self, system_config: Dict, login_type: str = "back") -> tuple[str, str]:
"""
选择登录URL
选择登录URL和登录类型
Args:
system_config: 系统配置字典
login_type: 登录类型,"front"或"back"
Returns:
登录URL
(登录URL, 登录类型) 元组
"""
self.logger.info("请选择登录地址:")
self.logger.info(" 1. 前台地址")
self.logger.info(" 2. 后台地址")
if login_type == "front":
login_url = system_config.get("system_front_url")
self.logger.info(f"使用前台地址登录: {login_url}")
else: # back
login_url = system_config.get("system_back_url")
self.logger.info(f"使用后台地址登录: {login_url}")
# 简化处理:默认使用后台地址
# 实际使用时可以通过命令行参数或交互式输入选择
login_url = system_config.get("system_back_url")
self.logger.info(f"使用后台地址登录: {login_url}")
return login_url
return login_url, login_type
def _initialize_browser(self) -> None:
"""初始化浏览器"""
......@@ -143,23 +145,21 @@ class TestCaseGeneratorMain:
self.logger.info("浏览器初始化完成")
def _login_system(self, login_url: str, system_config: Dict) -> None:
def _login_system(self, login_url: str, login_type: str) -> None:
"""
登录系统
登录系统(使用配置文件中的定位器)
Args:
login_url: 登录URL
system_config: 系统配置字典
login_type: 登录类型,"front"或"back"
"""
self.logger.info("正在登录系统...")
self.login_handler = LoginHandler(self.browser)
username = system_config.get("username")
password = system_config.get("password")
code = system_config.get("code", "csba")
# 使用配置管理器初始化登录处理器
self.login_handler = LoginHandler(self.browser, self.config_manager)
if not self.login_handler.login(login_url, username, password, code):
# 执行登录
if not self.login_handler.login(login_url, login_type):
raise Exception("登录失败")
# 初始化导航处理器
......@@ -279,7 +279,8 @@ def main():
# 创建并运行主控类
main_controller = TestCaseGeneratorMain(
system_config_path=args.system_config,
module_config_path=args.module_config
module_config_path=args.module_config,
login_type=args.login_type
)
try:
......
......@@ -11,23 +11,17 @@
- system_config.json文件格式示例:
```json
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
},
{
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
"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文件格式示例:
......
# _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
- **版本**: v1.1
- **最后更新**: 2026-03-06 (第二轮验证)
- **状态**: 持续修复中
---
## 问题概述
通过实际执行生成的测试用例,发现了定位器相关的关键问题,导致测试用例无法正常执行。
---
## 发现的问题清单
### 问题1: ID重复冲突 ⚠️ 严重
**问题描述**
- 不同的HTML元素共享相同的ID属性
- 导致通过ID无法唯一识别目标元素
**具体案例**
```javascript
// 区域管理 > 增值服务页面
"分类管理"按钮: id="set_room_list.value_add-create" class="el-button el-button--success"
"添加"按钮: id="set_room_list.value_add-create" class="el-button el-button--primary"
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤3: 点击【添加】按钮
- 所有使用该ID定位的测试步骤
**问题表现**
```json
{
"step": "点击【添加】按钮",
"locator_type": "ID",
"locator_value": "set_room_list.value_add-create",
"result": "失败 - 打开了'分类管理'对话框而非'添加商品'对话框"
}
```
**修复方案**
```diff
- {
- "locator_type": "ID",
- "locator_value": "set_room_list.value_add-create"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]"
```
**修复状态**: ✅ 已完成
---
### 问题2: XPath语法错误 ⚠️ 严重
**问题描述**
- 生成的XPath表达式使用了从0开始的索引
- 标准XPath索引从1开始
**具体案例**
```json
// 错误的XPath格式
"locator_value": "body/div[0]/div[1]/div[2]/div[1]/div[2]/input[1]"
// ^^^ 索引从0开始,这是错误的
```
**正确的XPath语法**
```xpath
// XPath索引从1开始,不是从0开始
body/div[1]/div[1]/div[2]/div[1]/div[2]/input[1] // 正确
body/div[0]/div[1]/div[2]/div[1]/div[2]/input[1] // 错误
// 推荐使用更简洁的定位方式
//input[@placeholder='请输入商品名称'] // 最佳
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤4-7: 表单输入操作
- `区域管理_增值服务_编辑.json` - 步骤4: 商品名称输入
- 所有使用`div[0]`格式的XPath定位
**修复方案**
```diff
// 输入框定位 - 使用属性定位
- {
- "locator_type": "XPATH",
- "locator_value": "body/div[0]/div[1]/div[2]/div[1]/div[2]/input[1]"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//input[@placeholder='请输入商品名称']"
// 按钮定位 - 使用文本和类名组合
- {
- "locator_type": "XPATH",
- "locator_value": "//*[@id=\"app\"]/div[1]/div[1]/div[1]/div[1]/div[1]/div[7]/div[1]/div[2]/span[1]/button[2]"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
```
**修复状态**: ✅ 已完成
---
### 问题3: 提示信息定位不精确 ⚠️ 中等
**问题描述**
- 提示信息的XPath定位过于宽泛,可能匹配到错误的消息
**具体案例**
```json
// 原始定位 - 可能匹配到任意消息
"locator_value": "//*[@id=\"app\"]//div[contains(@class, \"el-message\")][last()]"
```
**改进方案**
```diff
- {
- "locator_type": "XPATH",
- "locator_value": "//*[@id=\"app\"]//div[contains(@class, \"el-message\")][last()]"
+ {
+ "locator_type": "XPATH",
+ "locator_value": "//div[contains(@class, 'el-message') and contains(text(), '添加成功')]"
```
**修复状态**: ✅ 已完成
---
### 问题4: el-message元素验证失败 ⚠️ 中等
**问题描述**
- Element UI的消息提示(el-message)元素存在时间非常短(通常1-2秒)
- 当测试执行到验证步骤时,消息元素已经消失
- 更新的定位器 `//p[@class='el-message__content']` 也无法捕获到元素
**具体案例**
```json
// 所有三个测试用例的最后一步都失败
步骤9: 验证"添加成功"消息 - 消息已消失
步骤6: 验证"修改成功"消息 - 消息已消失
步骤5: 验证"删除成功"消息 - 消息已消失
```
**实际状态**
- 所有操作本身都成功执行
- 列表数据正确更新(计数变化、内容变化)
- 成功消息确实显示过,但在验证时已消失
**解决方案建议**
1. **使用显式等待**:在验证步骤前添加等待逻辑,等待消息出现
2. **使用快照验证**:在操作后立即保存页面状态快照
3. **使用数据验证**:验证列表数据变化而非提示消息
4. **延长消息显示时间**:修改Element UI配置(需要开发支持)
**修复状态**: ⚠️ 需要进一步优化
---
### 问题5: XPath文本匹配层级问题 ⚠️ 严重
**问题描述**
- `text()`函数只匹配元素的直接文本节点,不包含子元素的文本
- Element UI按钮的文本通常在`<span>`子元素内,不在button的直接文本中
**具体案例**
```javascript
// 按钮的实际结构
<button class="el-button el-button--primary" id="...">
<span>添加</span> <!-- 文本在span内,不是button的直接文本 -->
</button>
// 错误的XPath(无法匹配)
//button[contains(text(), '添加')]
// 正确的XPath
//button[.//text()='添加'] 或 //button[contains(., '添加')]
```
**影响范围**
- `区域管理_增值服务_添加.json` - 步骤3: 点击【添加】按钮
- 所有使用`contains(text(), '文本')`格式匹配按钮的场景
**正确修复方案**
```diff
- "locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]"
+ "locator_value": "//button[contains(@class, 'el-button--primary') and .//text()='添加']"
或者更简洁的写法:
+ "locator_value": "//button[contains(@class, 'el-button--primary') and contains(., '添加')]"
```
**修复状态**: ⚠️ 待修复
---
### 问题6: 确定按钮XPath定位失败 ⚠️ 严重
**问题描述**
- 确定按钮的XPath使用了与问题5相同的`text()`匹配方式
- 同时`el-dialog__footer`类名可能不存在
**具体案例**
```json
// JSON中的定位器
"locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]"
// 实际DOM中可能使用的是
<div class="dialog-footer">...</div> <!-- 注意没有el-前缀 -->
```
**修复方案**
需要同时修复两个问题:
1. 类名匹配
2. 文本匹配方式
**修复状态**: ⚠️ 待修复
---
## 修复后的定位器策略
### 推荐的定位器优先级
| 优先级 | 定位器类型 | 适用场景 | 示例 |
|--------|-----------|---------|------|
| 1 | ID | 唯一ID且不重复 | `id="unique-element"` |
| 2 | 属性定位 | 有唯一属性值 | `//input[@placeholder='唯一文本']` |
| 3 | 文本+属性组合 | 需要结合多个特征 | `//button[contains(text(), '确定') and contains(@class, 'primary')]"` |
| 4 | 文本定位 | 元素有唯一文本 | `//button[text()='确定']` |
| 5 | CSS选择器 | 复杂定位场景 | `button.el-button--primary` |
### XPath生成规范
**必须遵循的规则**
1. 索引从1开始:`div[1]` ✅,`div[0]`
2. 使用属性定位优于层级定位:`//input[@id='x']` ✅,`//div[1]/div[2]/input[1]` ⚠️
3. 使用相对路径优于绝对路径:`//input[@placeholder='x']` ✅,`/body/div[1]/.../input[1]`
4. 使用contains()匹配部分文本:`//button[contains(text(), '确定')]`
---
## 修复后的测试用例
### 1. 区域管理_增值服务_添加.json
```json
{
"name": "区域管理_增值服务_添加",
"para": [
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "点击【添加】按钮",
"locator_type": "XPATH",
"locator_value": "//button[contains(text(), '添加') and contains(@class, 'el-button--primary')]",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "输入商品名称",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='请输入商品名称']",
"element_type": "input",
"element_value": "测试商品001"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "点击商品分类选择框",
"locator_type": "XPATH",
"locator_value": "//input[@placeholder='请选择商品分类']",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "选择【商品】分类",
"locator_type": "XPATH",
"locator_value": "//li[contains(text(), '商品')]",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "输入商品描述",
"locator_type": "XPATH",
"locator_value": "//textarea[@placeholder='请输入商品描述']",
"element_type": "input",
"element_value": "自动化测试商品描述"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "点击【确定】按钮",
"locator_type": "XPATH",
"locator_value": "//div[contains(@class, 'el-dialog__footer')]//button[contains(text(), '确定') and contains(@class, 'el-button--primary')]",
"element_type": "click"
},
{
"page": "Backend/MeetingRoom/ServiceManage",
"step": "获取提示文本",
"locator_type": "XPATH",
"locator_value": "//div[contains(@class, 'el-message') and contains(text(), '添加成功')]",
"element_type": "getTips",
"element_value": "添加成功",
"expected_result": "添加成功"
}
]
}
```
### 2. 区域管理_增值服务_编辑.json
**关键修复点**:
- 编辑按钮定位:使用XPath匹配包含`edit`的ID和图标类名
- 确定按钮定位:与添加操作相同
### 3. 区域管理_增值服务_删除.json
**关键修复点**:
- 删除按钮定位:使用XPath匹配包含`delete`的ID和图标类名
- 确认对话框定位:使用`el-message-box`类名定位
---
## 代码修复建议
### 测试用例生成器需要添加的验证逻辑
```python
def validate_locator(locator_type: str, locator_value: str) -> tuple[bool, str]:
"""
验证定位器是否有效
Returns:
(is_valid, error_message)
"""
# 检查1: XPath索引是否从0开始(错误)
if locator_type == "XPATH" and "[0]" in locator_value:
return False, "XPath索引不能从0开始,应该从1开始"
# 检查2: ID定位器是否在当前页面唯一
if locator_type == "ID":
elements = driver.find_elements(By.ID, locator_value)
if len(elements) > 1:
return False, f"ID '{locator_value}' 不唯一,找到{len(elements)}个元素"
if len(elements) == 0:
return False, f"ID '{locator_value}' 未找到任何元素"
# 检查3: 验证XPath是否有效
if locator_type == "XPATH":
try:
elements = driver.find_elements(By.XPATH, locator_value)
if len(elements) == 0:
return False, f"XPath未找到任何元素: {locator_value}"
if len(elements) > 1:
return False, f"XPath找到{len(elements)}个元素,不唯一: {locator_value}"
except Exception as e:
return False, f"XPath语法错误: {str(e)}"
return True, ""
```
---
## 测试验证结果
### 第一轮验证结果
| 测试用例 | 修复前状态 | 修复后状态 | 备注 |
|---------|-----------|-----------|------|
| 区域管理_增值服务_添加 | ❌ 步骤3、4、8失败 | ✅ 操作成功 | 定位器修复有效 |
| 区域管理_增值服务_编辑 | ❌ 步骤3、4失败 | ✅ 操作成功 | 定位器修复有效 |
| 区域管理_增值服务_删除 | ⚠️ 步骤3可能失败 | ✅ 操作成功 | 定位器修复有效 |
### 第二轮验证结果(2026-03-06)
| 测试用例 | 操作执行 | 消息验证 | 遇到的问题 |
|---------|---------|---------|-----------|
| 区域管理_增值服务_添加 | ✅ 成功 | ❌ 定位器失败 | 问题5、6 |
| 区域管理_增值服务_编辑 | - | - | 待验证 |
| 区域管理_增值服务_删除 | - | - | 待验证 |
**详细问题**
- 步骤3(添加按钮):`contains(text(), '添加')` 无法匹配span内的文本
- 步骤8(确定按钮):类名`el-dialog__footer`可能不准确
- 步骤9(消息验证):`//p[@class='el-message__content']` 定位器失败
---
## 后续优化方向
### 1. 定位器智能选择
- 实现定位器优先级算法
- 当ID不唯一时,自动选择替代定位方式
### 2. XPath生成优化
- 修复索引从0开始的问题
- 优先生成属性定位而非层级定位
- 简化XPath表达式
### 3. 定位器验证机制
- 生成测试用例前进行定位器验证
- 对每个定位器进行唯一性检查
- 输出验证报告
### 4. 错误处理增强
- 当定位器不唯一时,提供多个备选方案
- 在测试用例中标注定位器稳定性评分
---
## 总结
### 第一轮验证(2026-03-06)
本次执行验证发现了测试用例生成过程中的三个关键问题:
1. **ID重复冲突** - 需要改用XPath或CSS选择器
2. **XPath语法错误** - 索引从0开始导致定位失败
3. **提示信息定位不精确** - 可能匹配到错误的消息
所有问题已通过重写测试用例文件得到修复。
### 第二轮验证(2026-03-06)
在用户更新了消息定位器后重新验证,发现了新的关键问题:
4. **el-message元素验证失败** - 消息存在时间太短(1-2秒),验证时已消失
5. **XPath文本匹配层级问题** - `text()`只匹配直接文本,不包含子元素文本
6. **确定按钮XPath定位失败** - 类名和文本匹配都存在问题
**当前状态**
- ✅ 操作执行成功(添加、编辑、删除都能完成)
- ✅ 列表数据验证成功
- ⚠️ 按钮定位器需要修复(问题5、6)
- ⚠️ 消息验证需要改进方案(问题4)
**建议优先修复**
1. 修复所有使用`contains(text(), 'xxx')`的定位器,改为`contains(., 'xxx')``.//text()='xxx'`
2. 验证`el-dialog__footer`类名的准确性
3. 为消息验证添加显式等待或使用数据验证替代
---
**文档结束**
# 自动化测试用例生成 - 执行问题修复文档
## 文档信息
- **创建日期**: 2026-03-06
- **版本**: 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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论