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

feat(smart-locate): 语义化用例生成器阶段D+——auto_login 跳过登录步骤 + 已有选择器步骤语义化升级 + data-key 属性选择器全局优先

- smart_locate_service:_is_login_step 跳过 auto_login 已覆盖的登录步骤(否则登录表单已消失致整条定位链中断);已有选择器执行失败时清空重定位一次(旧坏选择器可升级为 semantic);navigate 步骤优先处理(URL 不当 CSS 解析);STEP_FAIL_DUMP 失败现场诊断
- keyword_matcher:data-key 属性选择器纳入可点击候选 + 容器限定属性选择器全局优先(修复 div:visible:has-text 命中外层容器假成功)
- tests:新增 TestIsLoginStep / TestTryAttachSemantic,47/47 通过
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 c0cd02ae
......@@ -511,6 +511,9 @@ def match_element_by_keywords(
# 常见可点击元素(图标、自定义按钮)
'i[onclick]:visible, div[onclick]:visible, span[onclick]:visible, '
'div[class*="btn"]:visible, span[class*="btn"]:visible, '
# ⚠️ data-key 属性元素:抽屉/列表菜单项(如 item_content_item)常带 data-key 但无
# btn/block 等 class,不加入则关键字定位永远匹配不到(如 新建会议 → reserve_list.create)
'[data-key]:visible, '
# 微前端中的常见可点击元素
'.block:visible, p:visible'
)
......@@ -566,6 +569,7 @@ def match_element_by_keywords(
name = el.get_attribute('name') or ''
aria_label = el.get_attribute('aria-label') or ''
data_testid = el.get_attribute('data-testid') or el.get_attribute('data-test-id') or ''
el_data_key = el.get_attribute('data-key') or ''
class_name = el.get_attribute('class') or ''
# ==================== Phase 2: 归一化评分 ====================
......@@ -722,6 +726,21 @@ def match_element_by_keywords(
if container_info:
parent_selector = container_info.get('selector', '')
container_type = container_info.get('type', '')
# ⚠️ data-key 属性组合选择器(最高优先级):
# 文本选择器可能命中外层容器导致"点击无效果"(如首页快捷 block 被遮挡,
# div:visible:has-text 匹配到 main_item 容器而非可点的 item_content_item)。
# 元素有稳定 data-key 且位于容器内时,容器限定的属性选择器最精确
# (实测 .el-drawer [data-key="reserve_list.create"] 点击后进入 CreateMeeting 页面)。
if el_data_key and parent_selector:
attr_sel = f'{parent_selector} [data-key="{el_data_key}"]'
if not any(s['value'] == attr_sel for s in selectors):
selectors.insert(0, {
'type': 'css',
'value': attr_sel,
'confidence': 0.92,
'priority': 1,
'match_type': 'attribute'
})
if parent_selector:
for sel in list(selectors): # 使用 list() 复制,避免迭代中修改
sel_value = sel.get('value', '')
......@@ -779,9 +798,19 @@ def match_element_by_keywords(
# 返回所有候选元素和选择器(供 Claude 语义排序使用)
elements_list = [c['element'] for c in limited_candidates]
all_selectors = []
for c in limited_candidates:
all_selectors.extend(c['selectors'])
# ⚠️ data-key 属性选择器(priority=1, match_type=attribute)全局优先:
# 候选按元素分数降序连接,外层容器(如首页快捷 block)文本选择器分数更高会排前面,
# 但属性选择器更精确(容器限定),必须排最前,否则被 div:visible:has-text 假成功截断
# (实测 .el-drawer [data-key="reserve_list.create"] 点击进入 CreateMeeting 页面)
attr_selectors = [
s for c in limited_candidates for s in c['selectors']
if s.get('priority') == 1 and s.get('match_type') == 'attribute'
]
rest_selectors = [
s for c in limited_candidates for s in c['selectors']
if not (s.get('priority') == 1 and s.get('match_type') == 'attribute')
]
all_selectors = attr_selectors + rest_selectors
# 去重选择器
seen_selectors = set()
......
......@@ -355,3 +355,112 @@ class TestInferSemanticFromPattern:
{"type": "list_checkbox"},
_FakePage(['tr:has-text("北京展厅会议室")']))
assert sem["type"] == "row_checkbox"
# ==================== _is_login_step(auto_login 登录步骤跳过) ====================
class TestIsLoginStep:
def test_navigate_http(self, svc):
"""navigate 到 http 地址 → 登录步骤"""
step = {"name": "访问系统登陆页面", "action": "navigate",
"params": {"selector": "https://192.168.5.44"}}
assert svc._is_login_step(step)
def test_fill_username(self, svc):
assert svc._is_login_step(
{"name": "输入用户名:admin@xty", "action": "fill", "params": {}})
def test_fill_password(self, svc):
assert svc._is_login_step(
{"name": "输入密码", "action": "fill", "params": {}})
def test_captcha(self, svc):
assert svc._is_login_step(
{"name": "输入验证码:csba", "action": "fill", "params": {}})
def test_click_login(self, svc):
assert svc._is_login_step(
{"name": "点击登录", "action": "click", "params": {}})
def test_nav_menu_not_login(self, svc):
"""功能中心/首页等菜单导航不算登录步骤"""
assert not svc._is_login_step(
{"name": "点击【功能中心】展开", "action": "click", "params": {}})
assert not svc._is_login_step(
{"name": "进入首页仪表盘", "action": "click", "params": {}})
def test_normal_form_step_not_login(self, svc):
assert not svc._is_login_step(
{"name": "时间切换为:预定会议", "action": "click", "params": {}})
assert not svc._is_login_step(
{"name": "填写会议名称", "action": "fill", "params": {}})
# ==================== _try_attach_semantic(已有选择器步骤语义化升级) ====================
class TestTryAttachSemantic:
def _mk_result(self):
sel = '.el-tabs__item:has-text("预定会议")'
return {
'success': True,
'params': {'selector': sel},
'selectors': {
'primary': sel,
'candidates': [{'type': 'css', 'value': sel, 'confidence': 0.95, 'priority': 1}],
},
}
def test_pattern_tab_attaches(self, svc):
"""tab 模式步骤:已有选择器也能生成 semantic,解析链前置 + 原 selector 兜底"""
_with_scope(svc, "create_meeting", "创建会议")
svc._semantic_resolver = _FakeResolver(
{"tab": ['[role="tab"]:has-text("预定会议")', '.time_btn .btns']})
result = self._mk_result()
step = {"order": 12, "name": "时间切换为:预定会议", "action": "click",
"params": {"selector": '.el-tabs__item:has-text("预定会议")'}}
page = _FakePage(['.time_btn .btns', '[role="tab"]:has-text("预定会议")'])
svc._try_attach_semantic(result, step, page)
assert result['semantic']['type'] == 'tab'
assert result['semantic']['text'] == '预定会议'
assert result['semantic']['scope'] == 'create_meeting'
# 解析链前置,原 selector 保留兜底
assert result['params']['selector'] == '[role="tab"]:has-text("预定会议")'
cands = [c['value'] for c in result['selectors']['candidates']]
assert cands[0] == '[role="tab"]:has-text("预定会议")'
assert '.el-tabs__item:has-text("预定会议")' in cands
def test_static_button_attaches(self, svc):
"""非模式静态步骤:走语义解析优先路径附加 semantic"""
_with_scope(svc, "create_meeting", "创建会议")
svc._semantic_resolver = _FakeResolver(
{"button": ['button:has-text("确定创建")', 'text=确定创建']})
result = {
'success': True,
'params': {'selector': 'button:has-text("确定创建")'},
'selectors': {'primary': 'button:has-text("确定创建")',
'candidates': [{'type': 'css', 'value': 'button:has-text("确定创建")',
'confidence': 0.95, 'priority': 1}]},
}
step = {"order": 20, "name": "点击确定创建", "action": "click",
"params": {"selector": 'button:has-text("确定创建")'}}
page = _FakePage(['button:has-text("确定创建")', 'text=确定创建'])
svc._try_attach_semantic(result, step, page)
assert result['semantic']['type'] == 'button'
assert result['semantic']['text'] == '确定创建'
assert result['params']['selector'] == 'button:has-text("确定创建")'
def test_no_resolution_no_attach(self, svc):
"""解析链无命中 → 不附加 semantic,原 result 不变"""
_with_scope(svc, "create_meeting", "创建会议")
svc._semantic_resolver = _FakeResolver({"tab": []})
result = self._mk_result()
step = {"order": 12, "name": "时间切换为:预定会议", "action": "click",
"params": {"selector": '.el-tabs__item:has-text("预定会议")'}}
svc._try_attach_semantic(result, step, _FakePage([]))
assert 'semantic' not in result
assert result['params']['selector'] == '.el-tabs__item:has-text("预定会议")'
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论