面向前端/测试工程师的通用技法文。核心命题:"元素存在"不等于"用户点得到"。给出用真实指针事件 + 命中测试双重取证的验收级自测法,全部结论来自实测。
一、为什么"能点"要单独验
自动化里最省事、也最不可信的点击方式是"直接触发元素":
// 三种"假点击",都不代表用户真能点到
element.click() // 绕过命中测试,被遮挡也照样触发
el.dispatchEvent(new MouseEvent('click')) // 同上,连坐标都不看
locator.click({force:true}) // 显式跳过可操作性检查
这三者只证明"绑定了 click 监听",不证明"在真实坐标上,这个元素是最上层、可交互、没被遮挡、尺寸够点"。验收级自测要证明的恰恰是后者。
二、双证法:真实指针点击 + 命中测试
证据一:真实指针点击(走完整 hit-testing)
用自动化框架的鼠标 API 按坐标点击,而非元素级 .click():
const box = await el.boundingBox();
await page.mouse.click(box.x + box.width/2, box.y + box.height/2);
区别:坐标点击会经过浏览器的命中测试——若该点被别的元素遮挡、或元素不可见/被禁用,点击落在遮挡物上,目标监听根本不会触发。因此"坐标点击后断言状态变化"是一次端到端可点性证明。
判定标准:
- 通过:坐标点击后,预期的状态变化发生(如面板展开、
aria-expanded翻转、URL 改变、列表项增加)。 - 不通过:点击后状态无变化 → 元素被遮挡 / 命中的是别的层 / 事件未冒泡到监听点。
证据二:命中测试(elementFromPoint 反查)
点击前,先问浏览器"这个坐标最上层到底是谁":
const top = await page.evaluate(([x,y]) => {
const el = document.elementFromPoint(x, y);
return el ? {tag: el.tagName, id: el.id, cls: el.className} : null;
}, [cx, cy]);
判定标准:
- 通过:
elementFromPoint(cx,cy)返回的元素是目标本身或其子节点。 - 不通过:返回的是别的元素(常见为透明遮罩、伪元素、绝对定位的兄弟节点)→ 目标在该点点不到,即使它
click()能触发。
为什么两证都要
- 只有证据一:点击"碰巧"生效时,你可能没意识到命中的是父容器的委托监听,换个布局就失效。
- 只有证据二:坐标处是目标,但目标可能
pointer-events:none或监听没绑,命中了却无响应。 - 两证交叉:坐标处最上层是目标 且 坐标点击产生预期状态变化 → 才算"用户真能点到且点了有用"。
三、可复现最小步骤
步骤 1:枚举待验交互点,取坐标
做法:对每个关键交互(导航项、折叠 chip、提交按钮、标签页),取 boundingBox() 中心坐标;同时记录该元素 getComputedStyle 的 pointer-events、visibility、display。
判定标准:
- 通过:坐标非空、
pointer-events:auto、可见。 - 不通过:
boundingBox()返回 null(元素未渲染/尺寸为 0)→ 该交互点当前不存在,直接记为缺陷。
步骤 2:命中测试反查最上层
做法:对每个坐标跑 elementFromPoint,比对是否命中目标。
判定标准:
- 通过:命中目标或其子节点。
- 不通过:命中其它元素 → 标记"遮挡",附命中元素标识。
步骤 3:真实坐标点击 + 状态断言
做法:page.mouse.click(cx,cy) 后,等待并断言预期状态(用可观测变化,不用"没报错"当通过)。
判定标准:
- 通过:预期状态在超时窗口内出现。
- 不通过:超时未变化 → 标记"点而无响应"。
步骤 4(触控场景):尺寸达标核验
做法:对移动目标,断言 width>=44 && height>=44(公开触控建议值)。
判定标准:
- 通过:命中区域 ≥ 44×44。
- 不通过:小于阈值 → 标记"触控过小",附实测尺寸。
四、实测踩坑
el.click()全绿不代表可点:一次自测中元素级 click 全部触发成功,但坐标点击时某折叠控件因被透明 header 覆盖而点不到——elementFromPoint直接暴露了遮挡。教训:验收禁用元素级.click()作可点性证据,只作功能触发。- 委托监听造成的"假可点":点空白处因父级
click委托也触发了逻辑,误判"该点可交互"。→ 必须配合证据二确认坐标处最上层就是目标。 - 动画未就位就取坐标:
boundingBox()在过渡动画中取到中间态坐标,点击落偏。→ 点击前await expect(el).toBeVisible()或等待动画稳定。 pointer-events:none的图标层:按钮内<svg>设了pointer-events:none,elementFromPoint命中的是外层<button>——此时命中"子节点/父节点"都算通过,别误判为遮挡。
五、一句话纪律
功能测试证明"代码会响应",验收级自测要证明"用户在真实坐标上点得到、点了有用"。差别就在一次坐标点击 + 一次 elementFromPoint 反查。