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

feat(element-mapping): 元素映射表完整实施 (Phase 1-4)

Phase 1: 生成元素映射表 JSON (105个元素, 覆盖首页/功能中心/筛选器)
Phase 2: 实现 ElementMappingService (3级匹配: 精确/模糊/上下文)
Phase 3: 集成到 PlaywrightExecutor (最高优先级选择器来源)
Phase 4: 本地验证通过 (9/9单测 + 13/13集成测试)
Phase 5: 部署到服务器 192.168.5.60 (SCP + Docker restart)

同步修复:
- keyword_matcher: 父容器检测 + 组合选择器 + 评分归一化
- smart_locate_service: 执行级联(前置步骤失败跳过后续)
- base_simulator: 设备模拟优化
- HANDOFF_UI自动化.md: 更新完成状态
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 5d261143
...@@ -17,6 +17,10 @@ data/ ...@@ -17,6 +17,10 @@ data/
*.db *.db
*.sqlite3 *.sqlite3
# 元素映射表 JSON 需要纳入版本控制
!backend/app/data/
!backend/app/data/*.json
# Debug / temp # Debug / temp
backend/debug_*.png backend/debug_*.png
backend/scripts/debug_*.py backend/scripts/debug_*.py
......
此差异已折叠。
此差异已折叠。
...@@ -28,6 +28,7 @@ from playwright._impl._errors import TimeoutError as PlaywrightTimeoutError ...@@ -28,6 +28,7 @@ from playwright._impl._errors import TimeoutError as PlaywrightTimeoutError
from app.config import settings from app.config import settings
from app.utils.selector_mapper import SelectorMapper from app.utils.selector_mapper import SelectorMapper
from app.services.element_mapping_service import get_element_mapping_service
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -160,6 +161,9 @@ class PlaywrightExecutor: ...@@ -160,6 +161,9 @@ class PlaywrightExecutor:
# SelectorMapper 实例(用于 page_key + element_key 解析) # SelectorMapper 实例(用于 page_key + element_key 解析)
self._selector_mapper: SelectorMapper = SelectorMapper() self._selector_mapper: SelectorMapper = SelectorMapper()
# 元素映射表服务(最高优先级选择器来源,Phase 3)
self._element_mapping_service = get_element_mapping_service()
# 内部状态 # 内部状态
self._playwright: Optional[Playwright] = None self._playwright: Optional[Playwright] = None
self._browser: Optional[Browser] = None self._browser: Optional[Browser] = None
...@@ -920,6 +924,24 @@ class PlaywrightExecutor: ...@@ -920,6 +924,24 @@ class PlaywrightExecutor:
logger.info(f"⏳ 步骤 {step_result.order}/{params.get('_total_steps', '?')}: {step_result.name}") logger.info(f"⏳ 步骤 {step_result.order}/{params.get('_total_steps', '?')}: {step_result.name}")
# ==================== Phase 3: 元素映射表最高优先级 ====================
# 查询元素映射表,如命中则注入到 params 中作为最高优先级选择器
step_name = step.get("name", "")
step_action = step.get("action", "")
if self._element_mapping_service.is_enabled() and step_name:
mapping_result = self._element_mapping_service.get_selector(step_name, step_action)
if mapping_result:
mapping_selector = mapping_result["selector"]
logger.info(f"[元素映射表] 命中: {step_name} → {mapping_selector}")
# 将映射表选择器注入到 params 中作为最高优先级
existing_selectors = params.get("selectors")
if isinstance(existing_selectors, list) and existing_selectors:
params["selectors"] = [mapping_selector] + list(existing_selectors)
else:
params["selectors"] = [mapping_selector]
if not params.get("selector"):
params["selector"] = mapping_selector
# 单步重试循环 # 单步重试循环
attempt = 0 attempt = 0
max_attempts = self.step_retry_count + 1 # 至少执行一次 max_attempts = self.step_retry_count + 1 # 至少执行一次
......
此差异已折叠。
...@@ -132,13 +132,36 @@ class SmartLocateService: ...@@ -132,13 +132,36 @@ class SmartLocateService:
logger.warning(f"导航到菜单 '{navigate_menu}' 失败") logger.warning(f"导航到菜单 '{navigate_menu}' 失败")
# 4. 逐步骤定位 # 4. 逐步骤定位
skip_remaining = False
for step in steps: for step in steps:
# 如果前置步骤失败,跳过后续步骤
if skip_remaining:
result = {
'order': step.get('order', 0),
'name': step.get('name', ''),
'success': False,
'action': step.get('action', ''),
'params': step.get('params', {}),
'selectors': {'primary': None, 'candidates': []},
'message': '前置步骤失败,跳过执行'
}
results.append(result)
continue
result = self._locate_single_step(step) result = self._locate_single_step(step)
results.append(result) results.append(result)
# 如果定位成功且操作类型需要执行,则实际执行(确保页面状态正确) # 如果定位成功且操作类型需要执行,则实际执行(确保页面状态正确)
if result.get('success') and result.get('action') != 'navigate': if result.get('success') and result.get('action') != 'navigate':
self._execute_step(result) exec_success = self._execute_step(result)
if not exec_success:
# 执行失败,回退标记定位结果为失败
logger.warning(f"步骤 {step.get('order')} 执行验证失败,回退标记定位为失败")
result['success'] = False
result['exec_success'] = False
result['message'] = f'定位成功但执行验证失败: {result.get("message", "")}'
# 后续步骤全部跳过
skip_remaining = True
logger.info(f"智能定位完成: {len(results)} 个步骤") logger.info(f"智能定位完成: {len(results)} 个步骤")
...@@ -721,6 +744,7 @@ class SmartLocateService: ...@@ -721,6 +744,7 @@ class SmartLocateService:
def _execute_step(self, result: Dict[str, Any]) -> bool: def _execute_step(self, result: Dict[str, Any]) -> bool:
""" """
实际执行步骤(验证选择器是否有效) 实际执行步骤(验证选择器是否有效)
如果点击后预期弹窗未出现,自动尝试替代选择器
Args: Args:
result (Dict): 定位结果(包含 selector 和 params) result (Dict): 定位结果(包含 selector 和 params)
...@@ -731,6 +755,8 @@ class SmartLocateService: ...@@ -731,6 +755,8 @@ class SmartLocateService:
action = result.get('action', '') action = result.get('action', '')
params = result.get('params', {}) params = result.get('params', {})
selector = params.get('selector', '') selector = params.get('selector', '')
step_name = result.get('name', '')
order = result.get('order', 0)
if not selector: if not selector:
return False return False
...@@ -742,41 +768,107 @@ class SmartLocateService: ...@@ -742,41 +768,107 @@ class SmartLocateService:
if action == 'fill': if action == 'fill':
value = params.get('value', '') value = params.get('value', '')
# 等待元素可见
page.wait_for_selector(selector, timeout=5000) page.wait_for_selector(selector, timeout=5000)
# 填充
page.fill(selector, str(value)) page.fill(selector, str(value))
logger.debug(f"填充成功: {selector} = {value}") logger.debug(f"填充成功: {selector} = {value}")
return True
elif action == 'click': elif action == 'click':
# 等待元素可见 # 判断点击后是否预期打开弹窗/抽屉
page.wait_for_selector(selector, timeout=5000) dialog_keywords = ['新建', '编辑', '添加', '新增', '创建', '修改', '详情',
# 点击 '确定创建', '取消会议', '完成']
page.click(selector) expects_dialog = any(kw in step_name for kw in dialog_keywords)
logger.debug(f"点击成功: {selector}")
# 等待可能的页面变化 # 构建替代选择器列表(优先级从高到低)
page.wait_for_timeout(1000) click_selector_chain = [selector]
# 如果当前选择器是 div:has-text 格式,尝试补充 button:has-text
if 'div:visible:has-text' in selector or 'div:has-text' in selector:
import re
text_match = re.search(r'has-text\("([^"]+)"\)', selector)
if text_match:
btn_text = text_match.group(1)
click_selector_chain.append(f'button:has-text("{btn_text}")')
click_selector_chain.append(f'[role="button"]:has-text("{btn_text}")')
click_selector_chain.append(f'.el-button:has-text("{btn_text}")')
# 如果当前选择器是 input:visible(错误匹配),尝试从步骤名提取关键词
if 'input:visible' in selector:
click_selector_chain.append(f'button:has-text("{step_name}")')
# 逐级尝试点击
click_success = False
actual_selector_used = None
for sel in click_selector_chain:
try:
page.wait_for_selector(sel, timeout=5000)
page.click(sel)
page.wait_for_timeout(800)
logger.debug(f"点击成功: {sel}")
click_success = True
actual_selector_used = sel
break
except Exception as e:
logger.debug(f"选择器 {sel} 点击失败: {e}")
continue
if not click_success:
logger.warning(f"步骤 {order} 所有选择器点击均失败")
return False
# 更新 params 中的 selector 为实际使用的选择器
if actual_selector_used != selector:
result['params']['selector'] = actual_selector_used
logger.info(f"步骤 {order} 选择器已更新: {selector} -> {actual_selector_used}")
# ⚠️ 新增:如果是点击功能中心,等待抽屉打开 # === 页面状态验证 ===
if '功能中心' in result.get('name', ''): # 1. 检测是否是功能中心点击
if '功能中心' in step_name:
logger.debug("检测到功能中心点击,等待抽屉打开...") logger.debug("检测到功能中心点击,等待抽屉打开...")
try: try:
page.wait_for_selector('.el-drawer', timeout=5000) page.wait_for_selector('.el-drawer', timeout=8000)
page.wait_for_timeout(500) # 额外等待动画 page.wait_for_timeout(500)
logger.debug("功能中心抽屉已打开") logger.debug("功能中心抽屉已打开")
except Exception as e: except Exception as e:
logger.warning(f"等待抽屉打开失败: {e}") logger.warning(f"功能中心抽屉未打开,标记为失败: {e}")
return False
# 2. 检测弹窗是否出现(如果步骤名称包含弹窗关键词)
if expects_dialog:
dialog_opened = False
for dialog_sel in ['.el-dialog', '.el-drawer', '.el-message-box']:
try:
page.wait_for_selector(dialog_sel, timeout=3000)
dialog_opened = True
logger.debug(f"弹窗已打开: {dialog_sel}")
break
except Exception:
continue
if not dialog_opened:
logger.warning(f"步骤 '{step_name}' 点击后未检测到弹窗,所有选择器均失败")
# 重要:弹窗未打开,返回 False 触发 skip_remaining
return False
# 3. 检测页面是否跳转(如果步骤名称包含导航关键词)
nav_keywords = ['进入', '打开', '跳转', '导航']
if any(kw in step_name for kw in nav_keywords):
page.wait_for_timeout(1000)
return True
elif action == 'wait': elif action == 'wait':
timeout = params.get('timeout', 10000) timeout = params.get('timeout', 10000)
page.wait_for_selector(selector, timeout=timeout) page.wait_for_selector(selector, timeout=timeout)
logger.debug(f"等待成功: {selector}") logger.debug(f"等待成功: {selector}")
return True
elif action == 'select': elif action == 'select':
value = params.get('value', '') value = params.get('value', '')
page.wait_for_selector(selector, timeout=5000) page.wait_for_selector(selector, timeout=5000)
page.select_option(selector, str(value)) page.select_option(selector, str(value))
logger.debug(f"选择成功: {selector} = {value}") logger.debug(f"选择成功: {selector} = {value}")
return True
return True return True
......
...@@ -200,16 +200,10 @@ class BaseSimulator(ABC): ...@@ -200,16 +200,10 @@ class BaseSimulator(ABC):
# 从 topic_params 获取动态参数 # 从 topic_params 获取动态参数
company_id = self.topic_params.get("company_id", "001") company_id = self.topic_params.get("company_id", "001")
room_id = self.topic_params.get("room_id", "A101") room_id = self.topic_params.get("room_id", "A101")
app_token = self.topic_params.get("app_token", "AND-518-0001") app_token = self.topic_params.get("app_token", "ANP-0R5-0013")
conference_id = self.topic_params.get("conference_id", "") conference_id = self.topic_params.get("conference_id", "")
conference_name = self.topic_params.get("conference_name", "") conference_name = self.topic_params.get("conference_name", "")
# 内存和存储
total_mem = 1957
free_mem = random.randint(600, 900)
internal_total = 11075297280
internal_free = random.randint(9700000000, 11000000000)
# 系统时间 # 系统时间
sys_date_obj = datetime.now() sys_date_obj = datetime.now()
sys_date = sys_date_obj.strftime("%a %b %d %H:%M:%S GMT+08:00 %Y") sys_date = sys_date_obj.strftime("%a %b %d %H:%M:%S GMT+08:00 %Y")
...@@ -222,7 +216,75 @@ class BaseSimulator(ABC): ...@@ -222,7 +216,75 @@ class BaseSimulator(ABC):
seconds = seed % 60 seconds = seed % 60
sys_elapsed_realtime = f"{hours}:{minutes:02d}:{seconds:02d}" sys_elapsed_realtime = f"{hours}:{minutes:02d}:{seconds:02d}"
# 构建 buildInfo # 判断设备类型
is_paperless = self.device_type == "paperless"
# 无纸化设备:新格式
if is_paperless:
total_mem = 3781
free_mem = random.randint(1200, 1800)
internal_total = 53768880128
internal_free = random.randint(28000000000, 35000000000)
result_data = {
"result": {
"buildConfig": {
"FLAVOR": "normalPaperless",
"FLAVOR_FEATURE": "paperless",
"FLAVOR_APP_TYPE": "normal",
"BUILD_TYPE": "release",
"LIB_UB_GVIEW_VERSION_NAME": "0.3.414",
"LIB_UB_GVIEW_VERSION_CODE": 414,
"LIB_UB_GVIEW_BUILD_DATE": "20260805100311",
},
"faceVersion": "",
"wgtVersion": "0.0.32",
"deviceModel": "BAH3-W59",
"abiList": ["arm64-v8a", "armeabi-v7a", "armeabi"],
"androidId": self.device_id,
"appName": "无纸化2.0",
"appPackageName": "com.ubains.local.gviewer",
"appVersion": 414,
"appVersionName": "1.3.414",
"battery": round(random.uniform(0.3, 1.0), 1),
"bluetoothEnabled": False,
"camerasCount": 2,
"charging": True,
"density": 2,
"freeAndTotalMemory": f"{free_mem}M/{total_mem}M",
"internalAvailableSize": internal_free,
"internalTotalSize": internal_total,
"ipAddress": ".".join(str(random.randint(1, 255)) for _ in range(4)),
"macAddress": f"{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}:{random.randint(0,255):02X}",
"networkType": "NETWORK_WIFI",
"processCpuUsage": "ERR",
"resolution": "2000x1200",
"romName": "huawei",
"rooted": False,
"sdkVersionCode": 29,
"sdkVersionName": "10",
"sysDate": sys_date,
"sysDateStr": sys_date_str,
"sysElapsedRealtime": sys_elapsed_realtime,
"sysLanguage": "zh",
"sysSupportedSensors": [
"accelerometer-kx021", "mag-mmc5603", "orientation",
"als-tcs3707", "gravity", "linear Acceleration",
"rotation Vector", "HALL sensor", "uncalibrated Magnetic Field",
"significant Motion", "geomagnetic Rotation Vector",
"phonecall sensor"
],
"authCode": app_token,
"conferenceName": conference_name
}
}
else:
# 门口屏设备:原格式
total_mem = 1957
free_mem = random.randint(600, 900)
internal_total = 11075297280
internal_free = random.randint(9700000000, 11000000000)
build_info = { build_info = {
"appToken": app_token, "appToken": app_token,
"companyNumber": company_id, "companyNumber": company_id,
...@@ -249,7 +311,6 @@ class BaseSimulator(ABC): ...@@ -249,7 +311,6 @@ class BaseSimulator(ABC):
}) })
} }
# 构建 result 对象
result_data = { result_data = {
"result": { "result": {
"buildInfo": build_info, "buildInfo": build_info,
......
此差异已折叠。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Phase 4: 验证元素映射表在 PlaywrightExecutor 中的集成效果
模拟目标用例 case_13650e0406e64779b66e6fa5de35c24a 的步骤 7-9,
验证映射表能否正确命中并提供稳定选择器。
"""
import json
import logging
import os
import sys
# 添加 backend 到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
from app.services.element_mapping_service import ElementMappingService
def test_element_mapping():
"""验证元素映射表对目标用例步骤 7-9 的匹配效果"""
svc = ElementMappingService()
print("=" * 60)
print("元素映射表集成验证 (Phase 3 & 4)")
print("=" * 60)
print(f" 映射表启用: {svc.is_enabled()}")
print(f" 元素总数: {len(svc._elements)}")
print()
# 目标用例的关键步骤
test_cases = [
# (步骤描述, 动作, 预期映射键, 预期选择器)
("点击功能中心抽屉展开", "click",
"导航栏-功能中心抽屉开关", ".home_nav_left"),
("点击【会议预约】分类", "click",
"全部功能-筛选器-会议预约", "#reserve_enable"),
("点击【新建会议】按钮", "click",
"首页-常用功能-创建会议", '[data-key="reserve_list.create"]'),
# 额外测试:其他常见操作
("点击【数据统计】分类", "click",
"全部功能-筛选器-数据统计", "#data_enable"),
("点击【会议列表】", "click",
"首页-常用功能-会议列表", '[data-key="reserve_list.meeting"]'),
("点击【会议室】入口", "click",
"首页-常用功能-会议室", '[data-key="reserve_list.room"]'),
("点击【创建会议】", "click",
"首页-常用功能-创建会议", '[data-key="reserve_list.create"]'),
("点击【会议模板】", "click",
"首页-常用功能-会议模板", '[data-key="reserve_list.model"]'),
("点击【会议日程】", "click",
"首页-常用功能-会议日程", '[data-key="reserve_list.schedule"]'),
# 筛选器测试
("点击筛选器中的会议预约分类", "click",
"全部功能-筛选器-会议预约", "#reserve_enable"),
("点击筛选器中的数据统计分类", "click",
"全部功能-筛选器-数据统计", "#data_enable"),
# 导航栏抽屉
("点击功能中心抽屉开关", "click",
"导航栏-功能中心抽屉开关", ".home_nav_left"),
# 搜索框
("输入搜索关键词", "fill",
"功能中心-搜索输入框", ".menu_header .search input"),
]
passed = 0
failed = 0
for step_name, action, expected_key, expected_selector in test_cases:
result = svc.get_selector(step_name, action)
if result:
actual_key = result["key"]
actual_selector = result["selector"]
conf = result["confidence"]
# 断言:映射键和选择器都要匹配
key_ok = expected_key in actual_key or actual_key in expected_key
sel_ok = expected_selector == actual_selector
status = "[PASS]" if (key_ok and sel_ok) else "[FAIL]"
if "[PASS]" in status:
passed += 1
else:
failed += 1
print(f" {status} [{action:6s}] {step_name}")
print(f" 期望 -> key={expected_key}, sel={expected_selector}")
print(f" 实际 -> key={actual_key}, sel={actual_selector}, conf={conf}")
else:
failed += 1
print(f" [FAIL] [{action:6s}] {step_name}")
print(f" 期望 -> key={expected_key}, sel={expected_selector}")
print(f" 实际 -> 未命中")
print()
print("=" * 60)
print(f"结果: {passed}/{passed + failed} 通过, {failed} 失败")
print("=" * 60)
# 模拟 PlaywrightExecutor 集成效果
print()
print("=" * 60)
print("模拟 PlaywrightExecutor 集成效果")
print("=" * 60)
print()
# 模拟步骤 7-9 的执行流程
steps_7_9 = [
{"order": 7, "name": "点击功能中心抽屉展开", "action": "click",
"params": {"selectors": [".home_nav_left"], "selector": ".home_nav_left"}},
{"order": 8, "name": "点击【会议预约】分类", "action": "click",
"params": {"selectors": ["#reserve_enable"], "selector": "#reserve_enable"}},
{"order": 9, "name": "点击【新建会议】按钮", "action": "click",
"params": {"selectors": ['[data-key="reserve_list.create"]'],
"selector": '[data-key="reserve_list.create"]'}},
]
for step in steps_7_9:
step_name = step["name"]
step_action = step["action"]
mapping_result = svc.get_selector(step_name, step_action)
if mapping_result:
mapping_selector = mapping_result["selector"]
# 模拟注入到 params
existing_selectors = step["params"].get("selectors", [])
if existing_selectors:
final_selectors = [mapping_selector] + list(existing_selectors)
else:
final_selectors = [mapping_selector]
print(f" [PASS] 步骤 {step['order']}: {step_name}")
print(f" [映射表] 命中: {step_name} -> {mapping_selector}")
print(f" [params] 注入后 selectors: {final_selectors}")
print(f" [params] selector: {mapping_selector}")
else:
print(f" [WARN] 步骤 {step['order']}: {step_name}")
print(f" [映射表] 未命中,使用原有选择器: {step['params'].get('selectors')}")
print()
return failed == 0
if __name__ == "__main__":
success = test_element_mapping()
sys.exit(0 if success else 1)
\ No newline at end of file
#!/usr/bin/env python
"""验证服务器上元素映射表服务是否正常加载"""
import json
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.element_mapping_service import ElementMappingService
svc = ElementMappingService()
print(f"Enabled: {svc.is_enabled()}")
print(f"Elements count: {len(svc._elements)}")
keys = list(svc._elements.keys())
print(f"First 5 keys: {json.dumps(keys[:5], ensure_ascii=False)}")
# Test key matches
test_cases = [
"点击【会议预约】分类",
"点击【新建会议】按钮",
"点击功能中心抽屉展开",
"输入搜索关键词",
"点击【会议服务统计】",
]
for step in test_cases:
result = svc.get_selector(step, "click")
if result:
print(f" [OK] {step} -> {result['selector']} (conf={result['confidence']})")
else:
print(f" [MISS] {step} -> None")
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
元素映射表服务单元测试
验证 ElementMappingService 的三种匹配策略:
- 精确匹配评分:解决"会议预约"歧义(应匹配筛选器-会议预约 而非 会议服务统计)
- 模糊匹配(jieba + SequenceMatcher ≥0.7)
- 上下文辅助匹配
- 容错:JSON 缺失时自动禁用
"""
import json
import os
import sys
import tempfile
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))
from app.services.element_mapping_service import ElementMappingService
@pytest.fixture
def sample_mapping(tmp_path):
"""创建一份最小映射表用于测试(避免依赖完整 JSON)"""
data = {
"version": "1.0",
"updated_at": "2026-08-11",
"source": "test",
"elements": {
"导航栏-功能中心抽屉开关": {"selector": ".home_nav_left", "type": "css", "tier": "首页导航栏", "description": "打开功能中心抽屉"},
"全部功能-筛选器-会议预约": {"selector": "#reserve_enable", "type": "css", "tier": "功能中心-筛选器", "description": "会议预约"},
"全部功能-筛选器-数据统计": {"selector": "#data_enable", "type": "css", "tier": "功能中心-筛选器", "description": "数据统计"},
"首页-常用功能-创建会议": {"selector": '[data-key="reserve_list.create"]', "type": "css", "tier": "首页功能块", "description": "创建会议"},
"首页-常用功能-会议室": {"selector": '[data-key="reserve_list.room"]', "type": "css", "tier": "首页功能块", "description": "会议室"},
"首页-常用功能-会议列表": {"selector": '[data-key="reserve_list.meeting"]', "type": "css", "tier": "首页功能块", "description": "会议列表"},
"全部功能-数据统计-会议服务统计": {"selector": '[data-id="data_list.meeting_service"]', "type": "css", "tier": "功能中心-全部功能", "description": "会议服务统计"},
"全部功能-数据统计-会议统计": {"selector": '[data-id="data_list.meeting_total"]', "type": "css", "tier": "功能中心-全部功能", "description": "会议统计"},
"功能中心-搜索输入框": {"selector": ".menu_header .search input", "type": "css", "tier": "功能中心-搜索框", "description": "功能中心搜索框"},
},
}
mapping_file = tmp_path / "elements_mapping.json"
mapping_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return str(mapping_file)
def _make_service(mapping_file):
return ElementMappingService(mapping_file)
class TestExactMatch:
"""精确匹配测试 - 评分公式"""
def test_classification_meeting_preference(self, sample_mapping):
"""'会议预约'分类应匹配筛选器-会议预约,而非会议服务统计"""
svc = _make_service(sample_mapping)
result = svc.get_selector("点击【会议预约】分类", "click")
assert result is not None
assert result["selector"] == "#reserve_enable"
def test_click_meeting_service_preference(self, sample_mapping):
"""'会议服务统计'应匹配对应统计项"""
svc = _make_service(sample_mapping)
result = svc.get_selector("点击【会议服务统计】", "click")
assert result is not None
assert result["selector"] == '[data-id="data_list.meeting_service"]'
def test_create_meeting(self, sample_mapping):
"""'新建会议'应通过同义词匹配到创建会议"""
svc = _make_service(sample_mapping)
result = svc.get_selector("点击【新建会议】按钮", "click")
assert result is not None
assert result["selector"] == '[data-key="reserve_list.create"]'
def test_drawer(self, sample_mapping):
"""'功能中心抽屉'应匹配导航栏开关"""
svc = _make_service(sample_mapping)
result = svc.get_selector("点击功能中心抽屉展开", "click")
assert result is not None
assert result["selector"] == ".home_nav_left"
def test_search(self, sample_mapping):
"""搜索输入框匹配"""
svc = _make_service(sample_mapping)
result = svc.get_selector("输入搜索关键词", "fill")
assert result is not None
assert result["selector"] == ".menu_header .search input"
class TestFaultTolerance:
"""容错测试"""
def test_missing_file(self, tmp_path):
"""映射表文件不存在时,服务禁用但不抛异常"""
svc = ElementMappingService(str(tmp_path / "nonexistent.json"))
assert not svc.is_enabled()
assert svc.get_selector("点击【会议预约】分类", "click") is None
def test_invalid_json(self, tmp_path):
"""映射表 JSON 损坏时,服务禁用"""
bad_file = tmp_path / "bad.json"
bad_file.write_text("{invalid json", encoding="utf-8")
svc = ElementMappingService(str(bad_file))
assert not svc.is_enabled()
def test_empty_elements(self, tmp_path):
"""elements 为空时禁用"""
empty_file = tmp_path / "empty.json"
empty_file.write_text(json.dumps({"elements": {}}, ensure_ascii=False), encoding="utf-8")
svc = ElementMappingService(str(empty_file))
assert not svc.is_enabled()
assert svc.get_selector("点击任意", "click") is None
class TestNoMatch:
"""未命中时返回 None"""
def test_unrelated_step(self, sample_mapping):
"""映射表未覆盖的步骤返回 None"""
svc = _make_service(sample_mapping)
result = svc.get_selector("删除某条记录", "click")
assert result is None
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论