diff --git a/src/agent/explorer.py b/src/agent/explorer.py index 3aa141b..c4e6b0f 100644 --- a/src/agent/explorer.py +++ b/src/agent/explorer.py @@ -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("'", "\\'") diff --git a/src/reporter/generator.py b/src/reporter/generator.py index 377a9d9..f81c6d4 100644 --- a/src/reporter/generator.py +++ b/src/reporter/generator.py @@ -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'