提交 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()
......
......@@ -13,6 +13,7 @@ import logging
import os
import re
import base64
import json
from typing import List, Dict, Any, Optional
from datetime import datetime
......@@ -137,6 +138,12 @@ class SmartLocateService:
logger.warning("自动登录失败,可能影响后续步骤定位")
else:
logger.info("自动登录成功")
# ⚠️ 阶段6+:登录后等待首页 SPA 就绪(5.60 生产容器渲染较慢,
# 立即点击功能中心等菜单可能因前端事件尚未挂载而点击失效)
try:
self.executor._page.wait_for_timeout(3000)
except Exception:
pass
# 3. 导航到目标菜单
if navigate_menu:
......@@ -163,6 +170,24 @@ class SmartLocateService:
results.append(result)
continue
# ⚠️ 阶段6+:auto_login 已自动登录,跳过用例中的登录步骤
# (访问登录页/输入账号密码验证码/勾选协议/点击登录)。
# 否则登录表单已消失 → 执行验证失败 → skip_remaining 中断整条定位链,
# 导致表单步骤(如"时间切换为:预定会议")永远无法被定位/升级 semantic。
if auto_login and self._is_login_step(step):
logger.info(f"步骤 {step.get('order')} 已自动登录,跳过登录步骤")
results.append({
'order': step.get('order', 0),
'name': step.get('name', ''),
'success': True,
'action': step.get('action', ''),
'params': step.get('params', {}),
'selectors': {'primary': None, 'candidates': []},
'message': '已自动登录,跳过登录步骤'
})
seen_steps.append(step)
continue
result = self._locate_single_step(step, previous_steps=seen_steps)
results.append(result)
seen_steps.append(step)
......@@ -171,7 +196,46 @@ class SmartLocateService:
if result.get('success') and result.get('action') != 'navigate':
exec_success = self._execute_step(result)
if not exec_success:
# ⚠️ 阶段6+:已有选择器执行失败 → 清空选择器重新定位一次
# (旧用例的坏选择器如 div:visible:has-text 存在但不可点/语义无法升级时,
# 直接跳过后链必断;重定位走 keyword 路径产出新选择器 + semantic)
had_existing = bool((step.get('params') or {}).get('selector'))
if had_existing:
logger.warning(f"步骤 {step.get('order')} 已有选择器执行失败,尝试重新定位")
step_wo_sel = dict(step)
step_wo_sel['params'] = dict(step.get('params', {}))
step_wo_sel['params'].pop('selector', None)
try:
retry = self._locate_single_step(step_wo_sel, previous_steps=seen_steps)
except Exception as e:
logger.debug(f"步骤 {step.get('order')} 重新定位异常: {e}")
retry = None
if retry and retry.get('success') and self._execute_step(retry):
result = retry
result['message'] = f"重新定位成功: {result.get('message', '')}"
results[-1] = result
logger.info(f"步骤 {step.get('order')} 重新定位并执行成功")
# 替换结果后继续,不中断后续步骤
continue
# 执行失败,回退标记定位结果为失败
# 诊断 dump:记录执行失败时页面的真实状态(URL/表单值/表格行数),
# 用于排查异步加载时序导致的"元素暂时不存在"类失败
try:
_page = self.executor._page
_d = {'url': _page.url}
try:
_d['name_val'] = _page.locator('input[placeholder*="会议名称"]').input_value()
except Exception:
_d['name_val'] = 'NA'
try:
_d['table_rows'] = _page.locator('.meeting_check .el-table__row').count()
_d['target_rows'] = _page.locator('.meeting_check .el-table__row:has-text("北京展厅会议室")').count()
_d['table_text'] = (_page.locator('.meeting_check').inner_text() or '')[:150].replace('\n', ' | ')
except Exception as _e:
_d['table_err'] = str(_e)[:80]
logger.warning(f"STEP_FAIL_DUMP: {json.dumps(_d, ensure_ascii=False)}")
except Exception as _e:
logger.warning(f"STEP_FAIL_DUMP_ERR: {_e}")
logger.warning(f"步骤 {step.get('order')} 执行验证失败,回退标记定位为失败")
result['success'] = False
result['exec_success'] = False
......@@ -419,6 +483,32 @@ class SmartLocateService:
# ⚠️ 关键修复:先获取 page 对象,验证步骤需要使用
page = self.executor._page
# ⚠️ 阶段6+:navigate 步骤优先处理(selector 可能是 URL 而非 CSS 选择器,
# 不能走 wait_for_selector 校验,否则 URL 被当 CSS 解析报 "Unexpected token /")
if action == 'navigate':
# 检查是否已有 url 参数(params.url 或 params.selector 里的 http 地址)
url = params.get('url', '') or ''
if not url:
sel_candidate = str(params.get('selector', '') or '')
if 'http' in sel_candidate:
url = sel_candidate
if not url:
# 从步骤名称中提取 URL 或使用默认值
import re
url_match = re.search(r'https?://[^\s]+', name)
if url_match:
url = url_match.group(0)
else:
# 使用默认 URL
url = settings.TARGET_URL
result['success'] = True
result['params']['url'] = url
result['selectors'] = {'primary': url, 'candidates': []}
result['message'] = f'导航步骤: {url}'
logger.info(f"步骤 {order} 导航到: {url}")
return result
# ⚠️ 新增:如果步骤已有选择器,直接使用,不重新定位
existing_selector = params.get('selector', '')
if existing_selector:
......@@ -429,6 +519,12 @@ class SmartLocateService:
'candidates': [{'type': 'css', 'value': existing_selector, 'confidence': 0.95, 'priority': 1}]
}
result['message'] = f'使用已有选择器: {existing_selector}'
# ⚠️ 阶段6+:已有选择器步骤同样尝试语义化升级(生成 semantic + 前置已验证解析链),
# 让旧用例重跑智能定位即可升级为语义化执行。不改写用户 selector,保留为兜底。
if self._semantic_enabled():
self._try_attach_semantic(result, step, page)
# 验证选择器是否有效(增加等待时间)
try:
page.wait_for_selector(existing_selector, timeout=8000)
......@@ -441,27 +537,6 @@ class SmartLocateService:
result['message'] = f'使用已有选择器(验证超时): {existing_selector}'
return result
# navigate 类型:需要设置 url 参数
if action == 'navigate':
# 检查是否已有 url 参数
url = params.get('url', '')
if not url:
# 从步骤名称中提取 URL 或使用默认值
import re
url_match = re.search(r'https?://[^\s]+', name)
if url_match:
url = url_match.group(0)
else:
# 使用默认 URL
url = settings.TARGET_URL
result['success'] = True
result['params']['url'] = url
result['selectors'] = {'primary': url, 'candidates': []}
result['message'] = f'导航步骤: {url}'
logger.info(f"步骤 {order} 导航到: {url}")
return result
# ⚠️ 新增:登录相关步骤优先使用已验证的选择器
login_selectors = self._get_login_selector(name)
if login_selectors:
......@@ -591,7 +666,53 @@ class SmartLocateService:
logger.debug(f"搜索框选择器 {sel} 未找到: {e}")
continue
# 如果两种方式都没有找到
# ===== 方式3:目标文本可见验证 =====
# 详情页/展示页场景:无提示语、无搜索框,验证对象是页面上的目标文本
# (如"查看界面会议名称正确显示:自动化新建会议"→ 等待文本"自动化新建会议"出现)
# 从步骤名提取 ":X" 尾部值作为目标文本,等待 :has-text 出现即验证通过
# 适用:创建成功后跳转详情页/展示页,验证动态值(名称/编号)已在页面正确显示
_text_m = re.search(r'[::]\s*([^::]+?)\s*$', name)
if _text_m and _text_m.group(1).strip():
target_text = _text_m.group(1).strip()
# 过滤明显非文本值:过长描述/元素类词/含引号文本不作为目标文本
if (len(target_text) <= 30 and '"' not in target_text and '\\' not in target_text
and not any(w in target_text for w in ('按钮', '弹窗', '搜索框', '下拉框'))):
text_sel = f':has-text("{target_text}")'
try:
page.wait_for_selector(text_sel, timeout=20000)
logger.info(f"找到目标文本(详情页验证): {target_text}")
result['success'] = True
result['action'] = 'wait' # wait 类型:等待目标文本出现即验证通过
result['selectors'] = {
'primary': text_sel,
'candidates': [
{'type': 'css', 'value': text_sel, 'confidence': 0.90, 'priority': 1}
]
}
result['params']['selector'] = text_sel
result['params']['timeout'] = 20000
result['params']['verify_method'] = 'text_visible'
result['params']['expected_text'] = target_text # 记录目标文本
result['message'] = f'验证类步骤(目标文本可见),找到文本: {target_text}'
logger.info(f"步骤 {order} 定位成功(目标文本验证): {text_sel}")
# 截图
try:
screenshot_path = os.path.join(
self.screenshot_dir,
f"smart_locate_{order}_{datetime.now().strftime('%H%M%S')}.png"
)
page.screenshot(path=screenshot_path)
with open(screenshot_path, 'rb') as f:
result['screenshot'] = base64.b64encode(f.read()).decode('utf-8')
except Exception as e:
logger.debug(f"截图失败(非致命): {e}")
return result
except Exception as e:
logger.debug(f"目标文本 {target_text} 未出现: {e}")
# 如果三种方式都没有找到
logger.warning(f"步骤 {order} 为验证类但未找到提示元素或搜索框")
result['success'] = False
result['message'] = f'验证类步骤,但未找到操作结果提示元素或列表搜索框'
......@@ -891,7 +1012,29 @@ class SmartLocateService:
if 'input:visible' in selector:
click_selector_chain.append(f'button:has-text("{step_name}")')
# 点击前等待目标文本渲染:列表/表格异步加载兜底
# (旧 case 已有选择器如 .el-checkbox:has-text 常匹配 0 个;若表格数据仍在异步加载,
# 直接点击/重定位都会因"目标行暂不存在"失败——先等 has-text 目标文本出现,
# 确保后续点击链与 Fix5 重定位时表格已就绪)
try:
_tm = re.search(r'has-text\("([^"]+)"\)', selector)
if _tm:
# 用 :has-text 子串匹配更宽容:TD 内文本可能带空白/嵌套,
# 精确 text= 匹配常漏掉导致"表格已加载但等待超时"
# 目标系统(192.168.5.44)表格异步加载极慢且不稳,放宽到 25s
page.wait_for_selector(f':has-text("{_tm.group(1)}")', timeout=25000)
page.wait_for_timeout(300)
logger.debug(f"步骤 {order} 目标文本已渲染: {_tm.group(1)}")
except Exception:
pass
# 逐级尝试点击(每级:普通点击 → force 强点;缩短超时避免 30s 默认卡死)
# 记录点击前 URL,用于判断是否为整页路由跳转(跳转则无需等待弹窗)
url_before = ''
try:
url_before = page.url
except Exception:
pass
click_success = False
actual_selector_used = None
for sel in click_selector_chain:
......@@ -914,8 +1057,20 @@ class SmartLocateService:
actual_selector_used = sel
break
except Exception as e2:
logger.debug(f"选择器 {sel} 点击失败(普通+force): {e2}")
continue
# JS 直接触发 click(绕过 playwright actionability 检测):
# Element Plus 表格 checkbox 等元素普通/force 点击常超时
# (实测 .el-checkbox:has-text 匹配 0 个、JS el.click() 可成功勾选),
# 作为最后兜底(取第一个匹配)
try:
page.locator(sel).first.evaluate('el => el.click()')
page.wait_for_timeout(800)
logger.debug(f"点击成功(js): {sel}")
click_success = True
actual_selector_used = sel
break
except Exception as e3:
logger.debug(f"选择器 {sel} 点击失败(普通+force+js): {e3}")
continue
if not click_success:
logger.warning(f"步骤 {order} 所有选择器点击均失败")
......@@ -940,6 +1095,18 @@ class SmartLocateService:
# 2. 检测弹窗是否出现(如果步骤名称包含弹窗关键词)
if expects_dialog:
# 整页路由跳转豁免:步骤名含"新建/创建"等弹窗关键词,但实际点击是页面跳转
# (如 新建会议 → /meetingV3 整页路由,无 .el-dialog/.el-drawer),URL 变化视为成功
url_after = ''
try:
url_after = page.url
except Exception:
pass
url_navigated = bool(url_before and url_after and url_before != url_after)
if url_navigated:
logger.info(f"步骤 '{step_name}' 点击后 URL 变化(页面跳转),豁免弹窗检查: {url_before} -> {url_after}")
return True
dialog_opened = False
for dialog_sel in ['.el-dialog', '.el-drawer', '.el-message-box']:
try:
......@@ -1040,6 +1207,82 @@ class SmartLocateService:
semantic['nth'] = nth
return semantic
def _try_attach_semantic(self, result: Dict[str, Any], step: Dict[str, Any], page) -> None:
"""
已有选择器步骤的语义化升级:生成 semantic + 前置已验证的解析链
场景:旧用例所有步骤都有 params.selector(智能定位旧版产物),
_locate_single_step 的"已有选择器"分支会 early return,从不生成 semantic,
导致这些步骤永远停留在"裸选择器"执行,遇到 DOM 变更即失败(如自定义 tab)。
本方法仅附加 semantic 目标(不改写用户手动配置的 selector,保留为兜底),
并把语义解析链置于 candidates / params.selector 最前:
- 智能定位自身的 _execute_step 验证先点语义链 → 验证通过,链不中断
- 落库后执行期解析器同样按 semantic 解析 → 与示范用例同机制
Args:
result (Dict): 定位结果(就地更新 selectors/params/semantic/message)
step (Dict): 步骤(name/action/params)
page: Playwright 页面
"""
action = step.get('action', '')
name = step.get('name', '') or ''
order = step.get('order', 0)
if not self._semantic_enabled():
return
semantic = None
# 1) 模式步骤("xxx选择:yyy"/"xxx切换为:yyy")→ 模式语义(text=锚点值 + contains),
# 内部已做 resolve 校验 + row_checkbox→row 降级
pattern_result = self._preprocess_step_pattern(name, action)
if pattern_result and pattern_result['selectors']:
semantic = self._infer_semantic_from_pattern(step, pattern_result, page)
# 2) 非模式静态步骤 → 语义解析优先路径(构造候选 semantic → resolve 命中)
if not semantic:
sem_result = self._try_semantic_execute(page, step, self._get_page_scope(page))
if sem_result:
semantic = sem_result['semantic']
if not semantic:
return
# 解析 semantic → 拿到执行期选择器链,前置到候选(验证/执行都优先走语义链)
chain: List[str] = []
try:
from app.executors.ui_context import ExecutionContext
scope = semantic.get('scope')
ctx = ExecutionContext(dict(self.collected_parameters or {}))
sem_step: Dict[str, Any] = {
'name': str(semantic.get('text') or ''),
'action': 'click',
'params': {},
'semantic': semantic,
}
chain = self._semantic_resolver.resolve_step(page, sem_step, scope, ctx) or []
chain = [sel for sel in chain if sel]
except Exception as e:
logger.debug(f"步骤 {order} 语义解析链获取失败: {e}")
if chain:
existing_sel = result['params'].get('selector', '')
new_cands = [
{'type': 'css', 'value': sel, 'confidence': 0.95, 'priority': i + 1}
for i, sel in enumerate(chain)
]
# 原 selector 保留为最后兜底(去重)
if existing_sel and existing_sel not in chain:
new_cands.append({
'type': 'css', 'value': existing_sel,
'confidence': 0.8, 'priority': len(chain) + 1,
})
result['selectors']['primary'] = chain[0]
result['selectors']['candidates'] = new_cands
result['params']['selector'] = chain[0]
result['semantic'] = semantic
result['message'] = f'使用已有选择器 + 语义化升级: {result["params"].get("selector", "")}'
logger.info(f"步骤 {order} 已有选择器步骤语义化升级: {semantic}")
def _infer_semantic_from_pattern(self, step: Dict[str, Any], pattern_result: Dict[str, Any], page) -> Optional[Dict[str, Any]]:
"""模式匹配成功后生成 semantic(type 来自模式类型,contains 来自"xxx:值"锚点值)"""
name = step.get('name', '')
......@@ -1440,6 +1683,34 @@ class SmartLocateService:
return None
def _is_login_step(self, step: Dict[str, Any]) -> bool:
"""
判断步骤是否为登录流程步骤(auto_login 已自动登录时应跳过)
覆盖两类:
1. navigate 到登录页/目标页(selector 或 url 是 http 链接)
2. 登录表单操作(输入账号/密码/验证码、勾选协议、点击登录)
"""
name = step.get('name', '') or ''
action = step.get('action', '') or ''
params = step.get('params', {}) or {}
# 1. navigate 到 http 地址(登录页/目标页)——URL 不能当 CSS 校验,且 auto_login 已直达
if action == 'navigate':
sel = str(params.get('selector', '') or '')
url = str(params.get('url', '') or '')
if 'http' in sel or 'http' in url:
return True
# 2. 登录表单操作步骤(排除功能中心/首页等菜单导航,避免误跳过业务步骤)
login_kw = ('用户名', '账号', '手机号', '密码', '验证码', '协议', '勾选', '登录')
if any(kw in name for kw in login_kw):
if '功能中心' in name or '首页' in name or '菜单' in name:
return False
return True
return False
def _get_login_selector(self, step_name: str) -> Optional[str]:
"""
获取登录相关步骤和导航步骤的选择器(使用已验证的模板)
......
......@@ -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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论