自动化工具 上传、滚动、作用域:三个最容易写出「假成功」的步骤,我踩过的坑

w471176877 · 2026年10月12日 · 36 次阅读

E2E 里有两类步骤。一类是「找到元素点一下」,简单直接;另一类是文件上传、滚动、作用域定位——它们有状态、有时序、有副作用,写错了往往不会当场红,而是重试到超时,或者安静地「假成功」。

这篇把我自己踩过、以及在一次真跑里观察到的坑整理一下。三个动作,三种不同的坑法。

一、上传:chooser 模式别重复点

上传写起来就两个参数:upload: { fileIds, mode },mode 决定交互方式:

模式 定位谁 干什么
input 唯一的 input[type=file](可以是隐藏的,不能是禁用) 直接 setInputFiles 塞文件
chooser 上传按钮(人点的那颗) 先监听文件选择事件,再点击,然后设置文件

第一个坑就在 chooser:这个动作已经包含点击了。如果前面又加一步「点击上传按钮」,页面会弹出两次文件选择窗口,第二步必然超时——而且报错信息通常很难看懂(它在等一个永远不会出现的 chooser)。

其他几条边界:

  • fileIds 是 1~5 个不同的文件,单文件最多 20MB,多文件要求控件支持 multiple;
  • 保留原始字节:别压缩图片,也別把 Excel/Word 抽出来的文本当成原文件上传;
  • 文件选中 ≠ 上传成功:change 只是「文件被塞进去了」,后面必须断言文件列表、导入记录或需求要求的结果。这一点特别容易被「假成功」骗到——页面上文件名都显示出来了,服务端其实没收;
  • 用户没给文件、或文件不存在时,如实说缺什么,不要编造服务器路径和文件 ID。

chooser 模式跑通:已选择 customers.csv,并断言到业务提示「上传成功:customers.csv」

如果要跨项目复用(把用例连文件一起交给别人),含上传的 .testcase 要升到 version: 2,顶层带上 files,每份资源是 {id, name, mime, size, sha256, data}:data 是原始字节的标准 base64(不带 data: 前缀)、size 是原始字节数、sha256 是同一份字节的小写十六进制。这三项用代码算,别手写——导入时会真的校验大小、base64 和 SHA-256,然后在目标项目里重建文件 ID 并重写步骤引用。

二、滚动:先回答「滚的是谁、滚到什么程度」

滚动步骤是 scroll: { target, mode, axis?, distance? }:

target 要不要定位器 可用 mode
page 不要 by / toStart / toEnd
container 要:唯一且可见的实际滚动容器 by / toStart / toEnd
element 要:唯一、可见、已挂载(可在视口外) 只有 intoView

axis 默认 y;by 必须给非零 distance,范围 -10000~10000 px,正数向下/右;其他模式不要写 distance,intoView 也不要写 axis。

四个容易走偏的地方:

  1. 普通 click 已经会自动滚入视口,不需要为每次点击补一步 scroll;
  2. 这里是 DOM 滚动 / scrollIntoViewIfNeeded,不等于鼠标滚轮(wheel)测试,两者别混;
  3. toEnd 只到当前内容边界,不会自动遍历无限列表;虚拟列表目标还没挂载时要逐段滚并查新状态,连续不动就停下来看加载状态或定位器,不要盲重试;
  4. 滚动参数会进验收证据(快照里能看到 scrollable=x/y/xy、容器位置尺寸),改了距离/目标/模式,之前那次「通过」就作废,得重跑。

容器滚动跑通:scrollTop 0 → 1933,末条「客户记录 060」进入视口

三、作用域:它不是新策略,是「换查询根」

作用域的写法是给主定位器挂一个父容器:

{
  "strategy": "role", "value": "button", "role": "button", "name": "确定",
  "scope": { "strategy": "role", "value": "dialog", "role": "dialog", "name": "导入客户" }
}

等价于 page.getByRole('dialog',{name:'导入客户'}).getByRole('button',{name:'确定'})。引擎里就是一句「换根」:

const root = loc.scope ? resolveQuery(page, loc.scope) : page;

理解这一点,下面几条就好记了:

  • 父容器必须唯一,不要用 .first() 去掩盖多匹配;
  • 目标必须真在容器的 DOM 子树里。这条最容易翻车:portal 实现的下拉、气泡常挂在 body 上,视觉上「在弹窗里」,DOM 上不在——按视觉位置套弹窗作用域就会查不到;
  • 作用域支持的策略是 testid / role / label / placeholder / text / alt / title / css,不支持 xpath、response、websocket,也不支持递归 scope;
  • scope.value / scope.name 同样支持 {{变量}},本地预校验也必须替换,只换主定位器是不够的;
  • 页面整体滚动既没有 locator 也没有 scope;容器滚动和上传可以用 scope 限定目标;接口/WS 定位不用 scope。

界面上的对应操作是:步骤表里可以添加、展开编辑、折叠、单独拾取、删除作用域。拾取作用域只更新父容器并保留主目标;删除作用域也保留主目标,只是恢复成页面级查询。

弹窗内「确定」被正确命中:页面级同名按钮 2 个,父容器唯一 1 个,容器内目标 1 个

四、真跑一遍,观察到的四条

技能包里对这三类动作有专项参考,而且明确要求:简化 smoke 模板不支持 upload/scroll,必须实现等价语义,不能跳过之后记成通过。所以我写了个本地固件页面(一个文件输入 + 一个可滚动容器 + 一个带同名干扰按钮的弹窗),按参考的语义真跑了一遍:

上传文件: customers.csv | size = 69 | sha256 = 4d191591cea4ca26…
用例: 三大难动作冒烟 | 目标: hard-actions.html
  [1] upload(chooser) -> PASSED  (监听 filechooser → 点击 → setFiles,随后断言业务结果「上传成功:customers.csv」)
  [2] scroll(container,toEnd) -> PASSED  (scrollTop 0 → 1933,末条「客户记录 060」可见 = true)
  [3] scope(role=dialog[name=导入客户] → button[确定]) -> PASSED  (页面级同名按钮匹配 2 个;父容器唯一 1 个;容器内目标 1 个;点击后命中提示 = true)
  [4] 反例:不用 scope、直接取页面级 .first() -> 页面级匹配 2 个;结果:<div id="mask" class="on">…</div> intercepts pointer events

结果: 三类动作语义验证通过,截图已保存到 shots/

第 4 条是我顺手加的反例,也是这次最有意思的发现:不用作用域时,页面级 getByRole('button',{name:'确定'}).first() 匹配到的是弹窗外面那颗按钮,而它正被遮罩盖着——点击会一直等「可点击」,重试到超时才失败。

定位歧义的表现不一定是「点错元素」,还可能是「元素明明看得见、却被浮层拦截,点击一路重试到超时」。

真实项目里这类现象很常见:想点的按钮就在眼前,日志里却是 intercepts pointer events、element is not stable 这一类话。

不用作用域的反例:页面级「确定」被遮罩拦截,点击重试到超时

五、三条边界(比技巧更重要)

  1. 这三类动作不走通用 AI 自愈。 upload、scroll、assert 都被排除在外——自愈是给「元素挪了位置」准备的,而上传/滚动语义一改,验收证据就不成立;断言更不能靠换目标把失败改成通过。所以这三类只能一次写对,别指望兜底。
  2. 参数改了,旧证据作废。 改滚动距离/目标/模式、换上传文件,都必须重跑,「上次通过了」不算数。
  3. 验证不全就如实标注。 跑不了的部分(比如需要特定文件、需要登录)要明确指出「未验证」,不要换文件、改断言把失败掩盖过去——那比没有用例更糟。

步骤表里这些动作长什么样:每步有定位策略与说明列

六、写这三类步骤时的检查清单

  • 上传:mode 选对了吗?chooser 前面是不是又加了一次点击?断言到业务结果了吗(还是只断言文件选中)?
  • 上传包:version: 2 了吗?size/sha256 是从真实字节算的吗?fileIds 与资源一一对应吗?
  • 滚动:target 是哪个?by 的 distance 非零且在范围里吗?element 是不是用的 intoView?
  • 作用域:父容器唯一吗?目标真在它的 DOM 子树里吗(portal 检查)?用没用到不支持的策略?
  • 兜底:本地预校验有没有替换 scope 里的占位符?跑不了的部分有没有标「未验证」?

七、留个问题

你们的上传用例是怎么断言「真的传上去了」的?有没有被 portal 弹窗(下拉、气泡挂在 body 上)坑过?评论区聊聊,我也想看看别人家的处理方式。

参考:

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册。