UI 自动化:UI 自动化中的等待机制设计
如果要只挑一个最容易把 UI 自动化拖垮的问题,更可能选等待机制。因为很多脚本不是不能执行,而是执行结果随机。
最典型的症状是:
- 本地能过,CI 上偶尔失败
- 同一条脚本上午过、下午挂
- 多跑几次又好了
这种问题看起来像“环境抖动”,但真实根因很多时候是:脚本根本没有定义清楚“什么时候页面真的准备好了”。
所以等待机制不是语法细节,而是稳定性设计。
一、我为什么把等待看成状态建模,而不是时间控制
把等待写成固定时间时,本质上是在赌时间:
- 点完按钮,
sleep(2) - 提交后,
sleep(5) - 跳转后,
sleep(1)
这种方式最大的问题不是慢,而是它根本没有表达页面状态。你不知道:
- 页面是否已经可交互
- 表格是否真的渲染完成
- 提交后到底是业务完成了,还是只是 toast 先出来了
所以更愿意把等待理解成一组状态判断:
- 页面就绪
- 组件就绪
- 数据稳定
- 业务完成
二、在项目里怎么拆等待层次
真实项目里,等待不该只停留在“等元素出现”,而更适合拆成四层。
1. DOM 层等待
比如:
- 元素存在
- 元素可见
- 元素可点击
这一层只回答:节点能不能被操作。
2. 组件层等待
比如:
- 下拉框选项加载完成
- 表格停止 loading
- 弹窗动画结束
这一层回答:组件是否进入稳定交互状态。
3. 页面层等待
比如:
- 页面主区域渲染完成
- 关键数据区域不再是 skeleton
- 路由跳转真正完成
这一层回答:用户是否已经“到了正确页面”。
4. 业务层等待
比如:
- 新建任务真的出现在列表中
- 审核状态从“待处理”变为“已通过”
- 异步报表状态从“生成中”切到“已完成”
这一层才是真正能决定测试结论的部分。
很多脚本之所以脆,就是因为只做了第 1 层等待,然后直接去做第 4 层断言。
三、不同工具下,怎么实现等待
Playwright
我最常用的是:
locator.waitFor()- Playwright assertion 自带重试
- 组合条件等待
例如不是只等按钮出现,而是等:
- 按钮出现
- loading 消失
- 列表目标行出现
Selenium
会统一封装:
WebDriverWait- expected conditions
- 自定义 lambda 条件
因为 Selenium 如果不统一等待层,很容易在各个页面对象里散落大量等待碎片。
chromedp
会封装:
- 节点可见
- 文本出现
- 某个 JS 条件返回 true
chromedp 更底层,所以更需要你自己把等待抽象成公共能力。
四、一个真实页面为什么不能只等 loading 消失
很多后台页面都有这个特点:
- 页面骨架先渲染
- 列表 loading 消失
- 但真正的数据表格还在二次渲染
- 按钮样式已经出现,但点击后会被遮罩拦截
如果脚本只写“等待 loading 消失”,你会得到很多随机失败:
- 元素看起来存在,实际不能点击
- 表格刚出来,但数据还是旧的
- toast 已弹,列表还没更新
所以等待机制必须比“loading 没了”更贴近业务状态。
五、我在真实项目里最常遇到的等待场景
1. 搜索表格结果刷新
这类页面通常会经历:
- 点击搜索
- 请求发出
- 表格 loading
- 数据替换
这里不该只等按钮点击完成,而更该等:
- loading 消失
- 结果总数变化或目标行出现
2. 提交表单后的状态切换
很多页面提交后先弹出“成功”,再异步更新列表。如果只断 toast,很容易误判。
更稳妥的做法是:
- 等 toast 出现
- 等 toast 消失
- 等列表页完成刷新
- 等目标状态出现
3. 异步任务轮询
比如导出报表、任务执行、审批流同步。这类场景用固定 sleep 基本是最差方案。
更稳妥的做法是轮询等待:
- 每隔 2 秒检查一次状态
- 超过总超时直接失败
- 失败时保存当前状态和最近轮询结果
六、怎么避免等待逻辑污染业务 case
更稳妥的约束是让等待逻辑只放在三种地方:
- 页面对象
- 组件对象
waits公共模块
业务 case 里尽量不出现大量原始等待代码。
例如:
1 | await taskListPage.waitLoaded(); |
而不是:
1 | await page.waitForSelector(...); |
后者短期能跑,长期会越来越散。
七、一个更像实战的失败例子
我遇到过一个后台任务页,提交成功后会立即弹 toast,但列表刷新依赖另一个异步接口。最早脚本只写:
- 点击提交
- 等 toast 成功
- 断言列表出现任务
结果在 CI 上经常失败,因为 toast 比列表刷新快很多。
后来改成:
- 点击提交
- 等成功 toast 出现
- 等表格 loading 完整走完一轮
- 等目标任务名出现
- 如 10 秒内未出现,再抓当前表格首屏文本和截图
这一改,稳定性明显上升,而且失败后能更快分辨:
- 是任务没创建成功
- 还是列表刷新慢
- 还是脚本定位错了
八、等待机制和执行速度怎么平衡
等待写深了,常常会带来“执行会很慢”的担心。更常见的情况是,真正拖慢执行的通常不是等待本身,而是无效等待。
更合理的做法是:
- 默认短超时
- 对历史高波动页面单独放宽
- 把长轮询只保留给少量高价值场景
- smoke 任务只保留强信号等待
也就是说,不是所有页面都要跑最重的等待策略,而是按风险分层。
九、怎么判断等待设计是不是做对了
我主要看这些信号:
- 同一条脚本的随机失败是否明显下降
- CI 和本地执行差异是否缩小
- 失败后是否更容易定位到是数据慢、页面慢还是业务错
- 页面行为变更时,是否更多改动集中在页面对象层
如果这些没改善,说明等待设计大概率还停留在“换了一种写 sleep 的方式”。
十、结语
UI 自动化中的等待机制,本质上是在定义页面何时真正进入“可继续执行”的状态。它不是简单的时间控制,而是一套稳定状态建模能力。只有把 DOM、组件、页面、业务四层等待边界都理清,UI 自动化才会从“偶尔能跑”变成“可以长期信任”。