feat: 实现深入功能测试能力
Some checks failed
AI Web Tester CI / test (push) Has been cancelled

核心改进:
- 识别表单: DOM 发现支持 Input, Textarea, Select 元素识别
- 自动填充: 支持自动在输入框填入测试数据并记录
- 业务验证: 自动监听操作后的 Toast/Message (AntD, Element, Vben) 消息
- 报告增强: 报告中展示详细的功能交互日志和验证结果徽标
- 配置优化: 提高 '新增', '搜索', '保存' 等业务按钮的优先探索权重
This commit is contained in:
empty
2025-12-28 22:00:33 +08:00
parent 3b4c6b5296
commit 3447ea340a
2 changed files with 127 additions and 20 deletions

View File

@@ -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("'", "\\'")