用 YAML 写 UI 自动化测试,不再硬编码 selector
用 YAML 写 UI 自动化测试,不再硬编码 selector
为什么要换一种做法
之前写 UI 自动化测试,不管用 Selenium 还是 Playwright,核心都是硬编码 CSS selector 或 XPath。页面结构一改,selector 就失效,测试全红。维护成本很高——不是测试逻辑有问题,而是定位元素的方式太脆弱。
我想试一种不同的方式:不写 selector,用自然语言描述要操作的元素(比如"认领按钮"、"单号输入框"),让框架自己去页面上找。测试流程也不写代码,用 YAML 配置。
整体架构
项目大约 1300 行 Python,分成几个核心模块:
FlowEngine(约 700 行)是调度核心。加载 YAML 配置的流程定义,按步骤顺序执行。支持 13 种操作类型——navigate(跳转)、click(点击)、fill(填写)、wait(等待)、screenshot(截图)、dialog_confirm(弹窗确认)、keyboard_press(按键)、search_waybill(搜索)、click_workorder_item(列表项点击)、pause_for_user(暂停让人工确认)等。
SemanticMatcher(约 150 行)是元素匹配的核心。输入是一个自然语言描述(比如"认领"),输出是页面上最匹配的元素。
auth.py 处理登录态注入——把 JWT 写到 Playwright 的 cookie 或 localStorage 里。
waybill_store.py 管理测试数据——从 Excel 读取运单号,标记已用过的行,防止重复。
YAML 流程定义
测试流程写在 flows.yaml 里,一个流程就是一组步骤:
flows:
standard:
name: "工单标准流程"
steps:
- name: "打开首页"
action: "navigate"
url: "{base_url}"
- name: "确认弹窗"
action: "dialog_confirm"
target: "确认"
timeout: 20000
required: true
- name: "输入单号"
action: "search_waybill"
value: "{work_order_no}"
- name: "点击认领"
action: "click"
target: "认领"
timeout: 20000
- name: "保存截图"
action: "screenshot"
save_as: "result_{work_order_no}.png"
变量用 {base_url}、{work_order_no} 这种格式,运行时从上下文注入。required: false 标记的步骤失败了不会中断流程——比如弹窗有时候不出现,跳过就好。
元素配置在 config.yaml 里,把语义描述映射到关键词和类型:
elements:
claim_button:
keywords: ["认领", "接单"]
type: "button"
home_dialog:
keywords: ["确认", "弹窗"]
type: "button"
同一个按钮可能在不同版本的页面上叫"认领"或"接单",配置里写上同义词,匹配时任意命中一个就算找到了。
语义匹配怎么做的
SemanticMatcher 的匹配算法不复杂。拿到目标描述(比如"认领按钮")后,先拆成关键词("认领"、"按钮")。然后从页面的 ARIA 快照里提取所有可交互元素,对每个元素打分:
- 关键词完全匹配文本:+0.5
- 部分匹配(包含关系):+0.3
- 角色匹配(元素是 button 而且描述里有"按钮"):+0.2
- cursor 是 pointer(看起来可以点):+0.1
最后取得分最高且超过 0.3 阈值的元素。
这个方案不用 LLM,纯规则匹配,速度快。但适用范围有限——只能处理文本可见、描述比较明确的元素。对于没有文本的图标按钮(比如电话图标),还是得在配置里写特殊处理。
点击的多策略重试
click 操作不是直接调 Playwright 的 page.click(selector) 就完事。FlowEngine 里对 click 做了 15 种以上的匹配策略,依次尝试:
text=认领—— Playwright 的文本匹配button:has-text("认领")—— 限定 button 标签button:has-text("接单")—— 同义词[role="button"]:has-text("认领")—— ARIA role 匹配.ant-btn:has-text("认领")—— Ant Design 按钮样式类- header 区域的 button —— 针对右上角操作按钮
- 电话图标特殊处理 —— 针对无文本的 SVG 图标
全部失败才报错。每次失败的尝试都记日志,方便排查是哪一步差了一点。
这种做法的好处是对 UI 变更有一定容错——换了 CSS 类名不影响文本匹配,换了文本不影响 role 匹配。但维护策略列表本身也是成本。
端到端的执行流程
一次完整的测试跑下来分五步:
- 从 Excel 里取一个未用过的运单号,标记为已使用
- 用 API 用户的身份请求后端,创建一条任务单据,拿到单号
- 用 UI 用户的身份获取登录态(JWT),注入到 Playwright 的 cookie
- 打开浏览器,按 YAML 流程定义逐步执行——打开页面、确认弹窗、搜索工单、认领、拨打电话、完结
- 生成 Markdown 报告(每步的名称、操作、结果、耗时),截图保存到
reports/screenshots/
注意第 2 步和第 3 步用的是不同的用户。创建工单用 user_id: 800302(模拟客服提交),UI 操作用 user_id: 201967(模拟坐席认领)。这是为了还原真实的多角色场景。
踩的坑
语义匹配对无文本元素无能为力。 电话图标是一个 SVG,没有可见文本,也没有 aria-label。SemanticMatcher 根本匹配不到。最后给这个元素写了专门的 selector 策略(找 SVG 图标的父级按钮),等于回到了硬编码 selector 的老路。以后如果上 LLM 做视觉识别可能能解决,但目前还是逐个处理。
弹窗时序不确定。 页面加载后有时候会弹一个确认框,有时候不弹。如果 required: true,不弹的时候流程就卡住等超时。改成 required: false 后,不弹就跳过。但有时候弹窗加载得慢,等 20 秒刚跳过,弹窗出来了,挡住了后面的操作。后来加了一个更长的 wait_element 步骤专门等弹窗,等到了就关,等不到就继续。
Excel 并发写入冲突。 多个测试实例同时跑,都去 Excel 里取运单号,会互相覆盖"已用"标记。openpyxl 不支持文件锁。暂时的解法是每次只跑一个实例。真要并行的话得换成数据库或者队列。
登录态过期后的失败表现不明显。 JWT 过期后,Playwright 还是能打开页面,但页面内容是空的或者跳到了登录页。FlowEngine 的 click 操作会在空页面上找不到元素,报"element not found"。错误信息看起来像是 selector 的问题,实际上是登录态过期了。后来加了 Token 缓存和 TTL 检查(默认 1 小时),过期自动刷新。
现在的样子
1300 行 Python,依赖 Playwright 和 openpyxl。能跑通工单系统的标准流程和加急流程。相比硬编码 selector 的老方案,YAML 配置确实更容易改——换一个按钮文案只需要在配置里加一个同义词。但语义匹配的覆盖范围有限,遇到复杂交互(拖拽、多级菜单、Canvas)还是得写代码。
这个项目更像是一次尝试——验证"不写 selector 能不能做 UI 自动化"。结论是:简单场景可以,复杂场景还是得回到代码。

浙公网安备 33010602011771号