核心改进: - 识别表单: DOM 发现支持 Input, Textarea, Select 元素识别 - 自动填充: 支持自动在输入框填入测试数据并记录 - 业务验证: 自动监听操作后的 Toast/Message (AntD, Element, Vben) 消息 - 报告增强: 报告中展示详细的功能交互日志和验证结果徽标 - 配置优化: 提高 '新增', '搜索', '保存' 等业务按钮的优先探索权重
This commit is contained in:
@@ -27,10 +27,10 @@ class FeatureExplorer:
|
|||||||
# 默认配置
|
# 默认配置
|
||||||
self.config = {
|
self.config = {
|
||||||
"max_depth": 3,
|
"max_depth": 3,
|
||||||
"max_clicks": 30,
|
"max_clicks": 50,
|
||||||
"skip_patterns": [], # 完全跳过
|
"skip_patterns": [],
|
||||||
"dangerous_patterns": ["删除", "移除", "清空", "退出", "注销"], # 记录但不执行
|
"dangerous_patterns": ["删除", "移除", "清空", "退出", "注销"],
|
||||||
"focus_patterns": [], # 优先探索
|
"focus_patterns": ["新增", "添加", "创建", "搜索", "查询", "确定", "保存", "提交"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def explore(self, config: Dict = None) -> Dict[str, Any]:
|
def explore(self, config: Dict = None) -> Dict[str, Any]:
|
||||||
@@ -207,15 +207,35 @@ class FeatureExplorer:
|
|||||||
'.tabs-chrome__item', // Vben tabs (右侧标签页)
|
'.tabs-chrome__item', // Vben tabs (右侧标签页)
|
||||||
'.vben-tabs-content > div', // Vben tabs 容器
|
'.vben-tabs-content > div', // Vben tabs 容器
|
||||||
'.n-tabs-tab', // Naive UI tabs
|
'.n-tabs-tab', // Naive UI tabs
|
||||||
|
// 表单元素
|
||||||
|
'input:not([type="hidden"]):not([type="submit"]):not([type="button"])',
|
||||||
|
'textarea',
|
||||||
|
'select',
|
||||||
|
'.ant-input, .ant-select', // 框架特定
|
||||||
|
'.el-input__inner, .el-select',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of selectors) {
|
for (const selector of selectors) {
|
||||||
document.querySelectorAll(selector).forEach(el => {
|
document.querySelectorAll(selector).forEach(el => {
|
||||||
const text = el.textContent?.trim().substring(0, 50) || '';
|
// 改进名称提取:优先使用 textContent,其次使用 placeholder/aria-label
|
||||||
const key = text + el.tagName;
|
let text = el.textContent?.trim().substring(0, 50) || '';
|
||||||
|
if (!text && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT')) {
|
||||||
|
text = el.getAttribute('placeholder') ||
|
||||||
|
el.getAttribute('aria-label') ||
|
||||||
|
el.getAttribute('name') ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
// 尝试通过 label 关联查找名称
|
||||||
|
if (!text && el.id) {
|
||||||
|
const label = document.querySelector(`label[for="${el.id}"]`);
|
||||||
|
if (label) text = label.textContent.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = text + el.tagName + el.className;
|
||||||
|
|
||||||
if (!text || seen.has(key)) return;
|
if (!text || seen.has(key)) return;
|
||||||
if (text.length < 2 || text.length > 50) return;
|
if (text.length < 1 || text.length > 50) return;
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
if (rect.width <= 0 || rect.height <= 0) return;
|
if (rect.width <= 0 || rect.height <= 0) return;
|
||||||
@@ -235,11 +255,14 @@ class FeatureExplorer:
|
|||||||
cls.includes('ant-tabs-tab') ||
|
cls.includes('ant-tabs-tab') ||
|
||||||
cls.includes('el-tabs__item')) type = 'tab';
|
cls.includes('el-tabs__item')) type = 'tab';
|
||||||
|
|
||||||
|
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') type = 'form_input';
|
||||||
|
if (el.tagName === 'SELECT') type = 'form_select';
|
||||||
|
|
||||||
elements.push({
|
elements.push({
|
||||||
name: text,
|
name: text,
|
||||||
type: type,
|
type: type,
|
||||||
tagName: el.tagName,
|
tagName: el.tagName,
|
||||||
priority: type === 'navigation' ? 8 : 5,
|
priority: type === 'navigation' ? 8 : (type.startsWith('form_') ? 7 : 5),
|
||||||
x: Math.round(rect.left + rect.width / 2),
|
x: Math.round(rect.left + rect.width / 2),
|
||||||
y: Math.round(rect.top + rect.height / 2)
|
y: Math.round(rect.top + rect.height / 2)
|
||||||
});
|
});
|
||||||
@@ -379,30 +402,54 @@ class FeatureExplorer:
|
|||||||
self.action_log.append(action_record)
|
self.action_log.append(action_record)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 执行点击 - 优先使用 DOM 发现时预计算的坐标
|
# 执行操作 (点击或填充)
|
||||||
try:
|
try:
|
||||||
# 优先使用元素自带的坐标(DOM 发现时已计算)
|
# 优先使用元素自带的坐标
|
||||||
if "x" in element and "y" in element:
|
if "x" in element and "y" in element:
|
||||||
coords = (element["x"], element["y"])
|
coords = (element["x"], element["y"])
|
||||||
else:
|
else:
|
||||||
# 回退到名称查找
|
|
||||||
coords = self._find_element_by_name(name)
|
coords = self._find_element_by_name(name)
|
||||||
|
|
||||||
if coords:
|
if coords:
|
||||||
print(f" → 点击 ({coords[0]}, {coords[1]})")
|
if el_type == "form_input":
|
||||||
self.browser.click_at(coords[0], coords[1])
|
test_data = f"AutoTest_{name}_{click_num}"
|
||||||
|
print(f" → 填充表单: {name} = {test_data}")
|
||||||
|
self.browser.click_at(coords[0], coords[1])
|
||||||
|
self.browser.wait(200)
|
||||||
|
self.browser.press_key("Control+A")
|
||||||
|
self.browser.press_key("Backspace")
|
||||||
|
self.browser.page.keyboard.type(test_data)
|
||||||
|
action_record["action_type"] = "fill"
|
||||||
|
else:
|
||||||
|
print(f" → 点击 ({coords[0]}, {coords[1]})")
|
||||||
|
self.browser.click_at(coords[0], coords[1])
|
||||||
|
action_record["action_type"] = "click"
|
||||||
|
|
||||||
self.browser.wait(500)
|
self.browser.wait(500)
|
||||||
action_record["action_taken"] = True
|
action_record["action_taken"] = True
|
||||||
action_record["click_coords"] = coords
|
action_record["click_coords"] = coords
|
||||||
|
|
||||||
|
# 操作后的结果验证
|
||||||
|
result_info = self._check_action_result()
|
||||||
|
if result_info:
|
||||||
|
print(f" 📋 结果确认: {result_info['message']}")
|
||||||
|
action_record["result"] = result_info
|
||||||
|
if result_info["type"] == "error":
|
||||||
|
self.bug_list.append({
|
||||||
|
"type": "business_error",
|
||||||
|
"element": name,
|
||||||
|
"message": result_info["message"],
|
||||||
|
"url": before_url
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
print(f" ⚠️ 未找到元素")
|
print(f" ⚠️ 未找到元素")
|
||||||
action_record["success"] = False
|
action_record["success"] = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ 点击失败: {e}")
|
print(f" ❌ 操作失败: {e}")
|
||||||
action_record["success"] = False
|
action_record["success"] = False
|
||||||
action_record["error"] = str(e)
|
action_record["error"] = str(e)
|
||||||
self.bug_list.append({
|
self.bug_list.append({
|
||||||
"type": "click_failed",
|
"type": "action_failed",
|
||||||
"element": name,
|
"element": name,
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
"url": before_url
|
"url": before_url
|
||||||
@@ -426,8 +473,42 @@ class FeatureExplorer:
|
|||||||
|
|
||||||
self.action_log.append(action_record)
|
self.action_log.append(action_record)
|
||||||
|
|
||||||
def _find_element_by_name(self, name: str) -> Optional[tuple]:
|
def _check_action_result(self) -> Optional[Dict]:
|
||||||
"""通过元素名称/文本查找坐标(使用 DOM 而非 AI,更快)"""
|
"""检查操作后的结果(如消息提示)"""
|
||||||
|
try:
|
||||||
|
# 检测常见的消息提示组件 (AntD, Element, Vben)
|
||||||
|
result = self.browser.page.evaluate('''
|
||||||
|
() => {
|
||||||
|
// 查找包含成功、错误关键词的消息框
|
||||||
|
const messageSelectors = [
|
||||||
|
'.ant-message-notice',
|
||||||
|
'.el-message',
|
||||||
|
'.vben-basic-title',
|
||||||
|
'.n-message',
|
||||||
|
'[role="alert"]',
|
||||||
|
'.message-box'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const sel of messageSelectors) {
|
||||||
|
const msg = document.querySelector(sel);
|
||||||
|
if (msg && msg.innerText) {
|
||||||
|
const text = msg.innerText;
|
||||||
|
let type = 'info';
|
||||||
|
if (text.includes('成功') || text.includes('Success')) type = 'success';
|
||||||
|
if (text.includes('失败') || text.includes('错误') || text.includes('Error') || text.includes('fail')) type = 'error';
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: text,
|
||||||
|
type: type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
return result
|
||||||
|
except:
|
||||||
|
return None
|
||||||
# 转义特殊字符
|
# 转义特殊字符
|
||||||
escaped_name = name.replace("\\", "\\\\").replace('"', '\\"').replace("'", "\\'")
|
escaped_name = name.replace("\\", "\\\\").replace('"', '\\"').replace("'", "\\'")
|
||||||
|
|
||||||
|
|||||||
@@ -295,15 +295,41 @@ class ReportGenerator:
|
|||||||
log_html = ""
|
log_html = ""
|
||||||
for log in action_log:
|
for log in action_log:
|
||||||
el = log.get("element", {})
|
el = log.get("element", {})
|
||||||
status = "⏭️ 跳过" if log.get("skipped") else ("✅" if log.get("success") else "❌")
|
action_type = log.get("action_type", "click")
|
||||||
|
|
||||||
|
# 推断状态和图标
|
||||||
|
if log.get("skipped"):
|
||||||
|
status_icon = "⏭️"
|
||||||
|
status_text = f"跳过 ({log.get('skip_reason', '')})"
|
||||||
|
elif not log.get("success"):
|
||||||
|
status_icon = "❌"
|
||||||
|
status_text = "失败"
|
||||||
|
else:
|
||||||
|
status_icon = "✅"
|
||||||
|
status_text = "填充" if action_type == "fill" else ("跳转" if log.get("url_changed") else "点击")
|
||||||
|
|
||||||
|
# 结果反馈
|
||||||
|
result_info = log.get("result")
|
||||||
|
result_html = ""
|
||||||
|
if result_info:
|
||||||
|
res_type = result_info.get("type", "info")
|
||||||
|
result_html = f'<div class="result-badge {res_type}">{result_info.get("message")}</div>'
|
||||||
|
|
||||||
screenshot = log.get("screenshot_after", "")
|
screenshot = log.get("screenshot_after", "")
|
||||||
screenshot_html = f'<img src="data:image/png;base64,{screenshot}" class="log-screenshot">' if screenshot else ""
|
screenshot_html = f'<img src="data:image/png;base64,{screenshot}" class="log-screenshot">' if screenshot else ""
|
||||||
|
|
||||||
|
details = ""
|
||||||
|
if action_type == "fill":
|
||||||
|
details = f'<div class="log-details">输入: {el.get("name", "")}</div>'
|
||||||
|
|
||||||
log_html += f'''
|
log_html += f'''
|
||||||
<div class="log-item">
|
<div class="log-item">
|
||||||
<span class="log-step">{log.get("step", 0)}</span>
|
<span class="log-step">{log.get("step", 0)}</span>
|
||||||
<span class="log-name">{el.get("name", "")}</span>
|
<div class="log-content">
|
||||||
<span class="log-status">{status}</span>
|
<span class="log-name">{el.get("name", "")} {status_icon}</span>
|
||||||
|
<span class="log-status">{status_text} {result_html}</span>
|
||||||
|
{details}
|
||||||
|
</div>
|
||||||
{screenshot_html}
|
{screenshot_html}
|
||||||
</div>'''
|
</div>'''
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user