核心改进: - 识别表单: DOM 发现支持 Input, Textarea, Select 元素识别 - 自动填充: 支持自动在输入框填入测试数据并记录 - 业务验证: 自动监听操作后的 Toast/Message (AntD, Element, Vben) 消息 - 报告增强: 报告中展示详细的功能交互日志和验证结果徽标 - 配置优化: 提高 '新增', '搜索', '保存' 等业务按钮的优先探索权重
This commit is contained in:
@@ -27,10 +27,10 @@ class FeatureExplorer:
|
||||
# 默认配置
|
||||
self.config = {
|
||||
"max_depth": 3,
|
||||
"max_clicks": 30,
|
||||
"skip_patterns": [], # 完全跳过
|
||||
"dangerous_patterns": ["删除", "移除", "清空", "退出", "注销"], # 记录但不执行
|
||||
"focus_patterns": [], # 优先探索
|
||||
"max_clicks": 50,
|
||||
"skip_patterns": [],
|
||||
"dangerous_patterns": ["删除", "移除", "清空", "退出", "注销"],
|
||||
"focus_patterns": ["新增", "添加", "创建", "搜索", "查询", "确定", "保存", "提交"],
|
||||
}
|
||||
|
||||
def explore(self, config: Dict = None) -> Dict[str, Any]:
|
||||
@@ -207,15 +207,35 @@ class FeatureExplorer:
|
||||
'.tabs-chrome__item', // Vben tabs (右侧标签页)
|
||||
'.vben-tabs-content > div', // Vben 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) {
|
||||
document.querySelectorAll(selector).forEach(el => {
|
||||
const text = el.textContent?.trim().substring(0, 50) || '';
|
||||
const key = text + el.tagName;
|
||||
// 改进名称提取:优先使用 textContent,其次使用 placeholder/aria-label
|
||||
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.length < 2 || text.length > 50) return;
|
||||
if (text.length < 1 || text.length > 50) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) return;
|
||||
@@ -235,11 +255,14 @@ class FeatureExplorer:
|
||||
cls.includes('ant-tabs-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({
|
||||
name: text,
|
||||
type: type,
|
||||
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),
|
||||
y: Math.round(rect.top + rect.height / 2)
|
||||
});
|
||||
@@ -379,30 +402,54 @@ class FeatureExplorer:
|
||||
self.action_log.append(action_record)
|
||||
return
|
||||
|
||||
# 执行点击 - 优先使用 DOM 发现时预计算的坐标
|
||||
# 执行操作 (点击或填充)
|
||||
try:
|
||||
# 优先使用元素自带的坐标(DOM 发现时已计算)
|
||||
# 优先使用元素自带的坐标
|
||||
if "x" in element and "y" in element:
|
||||
coords = (element["x"], element["y"])
|
||||
else:
|
||||
# 回退到名称查找
|
||||
coords = self._find_element_by_name(name)
|
||||
|
||||
if coords:
|
||||
print(f" → 点击 ({coords[0]}, {coords[1]})")
|
||||
self.browser.click_at(coords[0], coords[1])
|
||||
if el_type == "form_input":
|
||||
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)
|
||||
action_record["action_taken"] = True
|
||||
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:
|
||||
print(f" ⚠️ 未找到元素")
|
||||
action_record["success"] = False
|
||||
except Exception as e:
|
||||
print(f" ❌ 点击失败: {e}")
|
||||
print(f" ❌ 操作失败: {e}")
|
||||
action_record["success"] = False
|
||||
action_record["error"] = str(e)
|
||||
self.bug_list.append({
|
||||
"type": "click_failed",
|
||||
"type": "action_failed",
|
||||
"element": name,
|
||||
"error": str(e),
|
||||
"url": before_url
|
||||
@@ -426,8 +473,42 @@ class FeatureExplorer:
|
||||
|
||||
self.action_log.append(action_record)
|
||||
|
||||
def _find_element_by_name(self, name: str) -> Optional[tuple]:
|
||||
"""通过元素名称/文本查找坐标(使用 DOM 而非 AI,更快)"""
|
||||
def _check_action_result(self) -> Optional[Dict]:
|
||||
"""检查操作后的结果(如消息提示)"""
|
||||
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("'", "\\'")
|
||||
|
||||
|
||||
@@ -295,15 +295,41 @@ class ReportGenerator:
|
||||
log_html = ""
|
||||
for log in action_log:
|
||||
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_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'''
|
||||
<div class="log-item">
|
||||
<span class="log-step">{log.get("step", 0)}</span>
|
||||
<span class="log-name">{el.get("name", "")}</span>
|
||||
<span class="log-status">{status}</span>
|
||||
<div class="log-content">
|
||||
<span class="log-name">{el.get("name", "")} {status_icon}</span>
|
||||
<span class="log-status">{status_text} {result_html}</span>
|
||||
{details}
|
||||
</div>
|
||||
{screenshot_html}
|
||||
</div>'''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user