Playwright Mouse API 详解:用 page.mouse 精确模拟点击、双击与滚轮滚动
本文围绕 Playwright 的 Mouse API 参考文档 展开,讲清 page.mouse 六个核心方法(click、dblclick、move、down、up、wheel)的坐标系约定、全部参数默认值与底层事件派发机制。读完后,你不仅能正确使用这套低级鼠标 API 完成点击、拖拽类交互和程序化滚动,还能结合仓库源码理解每次调用最终如何变成浏览器端的真实鼠标事件。
Mouse 类与坐标系:以视口左上角为原点
Mouse 类以主框架(main frame)视口左上角为原点、以 CSS 像素为单位操作鼠标。所有 x、y 参数都是相对于主框架视口的 CSS 像素坐标,而不是某个元素坐标——这与 locator.click() 这类"点击元素中心"的 API 有本质区别,Mouse 是真正操作光标的低级接口。
每个 page 对象都拥有一个独立的 Mouse 实例,通过 Page.mouse 属性访问。以 JS 为例,用 page.mouse 描绘一个 100x100 的方形的完整示例(官方文档中的原始示例):
// Using ‘page.mouse’ to trace a 100x100 square.
await page.mouse.move(0, 0);
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.move(100, 100);
await page.mouse.move(100, 0);
await page.mouse.move(0, 0);
await page.mouse.up();
同一操作在其他语言中的等价写法:
// Java
page.mouse().move(0, 0);
page.mouse().down();
page.mouse().move(0, 100);
page.mouse().move(100, 100);
page.mouse().move(100, 0);
page.mouse().move(0, 0);
page.mouse().up();
# Python(同步与 async 两种写法均适用,async 时加 await)
page.mouse.move(0, 0)
page.mouse.down()
page.mouse.move(0, 100)
page.mouse.move(100, 100)
page.mouse.move(100, 0)
page.mouse.move(0, 0)
page.mouse.up()
// C#
await Page.Mouse.MoveAsync(0, 0);
await Page.Mouse.DownAsync();
await Page.Mouse.MoveAsync(0, 100);
await Page.Mouse.MoveAsync(100, 100);
await Page.Mouse.MoveAsync(100, 0);
await Page.Mouse.MoveAsync(0, 0);
await Page.Mouse.UpAsync();
这个"先 down() 再多次 move() 最后 up()"的组合正是实现按住鼠标拖出轨迹(drawing、拖拽、圈选)的标准模式,因为 down 之后光标移动会携带 mousedown 状态。
调试提示:如果想观察鼠标移动到了哪里,可以用 Trace Viewer 或 Playwright Inspector,每次鼠标操作都会在轨迹图中显示一个红点标记鼠标位置。红点机制来自服务端的 instrumentation 回调,见文末"事件派发链路"一节。
Mouse.click:move + down + up 的快捷方式
click(x, y, options) 是 move、down、up 三个操作的组合快捷方式,参数与默认值如下(参数说明源自 docs/src/api/params.md):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
x |
float | 必填 | 相对主框架视口左上角的 X 坐标(CSS 像素) |
y |
float | 必填 | 相对主框架视口左上角的 Y 坐标(CSS 像素) |
button |
"left" | "middle" | "right" |
left |
使用的鼠标按键 |
clickCount |
int | 1 | 对应 DOM UIEvent.detail 的点击次数 |
delay |
float | 0 | mousedown 与 mouseup 之间的等待毫秒数 |
delay 的意义在于模拟真实用户"按住一段时间"的点击(部分页面用 press 时长区分点击与长按)。从 服务端 Mouse.click 的实现 可以看到两条执行路径:
- 设置了
delay时:先move到位,然后对每个clickCount依次执行down → 等待 delay 毫秒 → up,中间点击之间也插入delay间隔; - 未设置
delay时(默认路径):Playwright 将 move、down、up 尽量并发发射(Promise.all),以最快完成整个点击序列;只有当steps > 1时才会先等待移动插值完成再按键。
这一细节解释了为什么默认的 click 非常快——它并不会人为放慢节奏,需要"拟人"节奏时请显式传 delay 和 steps。
客户端侧,Mouse.click 只是把参数转发到 page 的通信通道,真正的动作编排发生在驱动端(server/input.ts)。
Mouse.dblclick:双键按下与 clickCount=2
dblclick(x, y, options) 等价于 move → down → up → down → up 两个完整点击序列。它支持的选项是 click 的严格子集:
x、y:同上,主框架视口坐标(CSS 像素);button:默认left;delay:默认 0。
从 客户端实现 看,dblclick 并非独立协议消息,而是直接调用 click(x, y, { ...options, clickCount: 2 })——即通过 clickCount: 2 让浏览器把第二次 mousedown 识别为双击(detail = 2),这正是 DOM 中 dblclick 事件能被页面正确触发的关键。C# 中该方法别名为 DblClickAsync。
Mouse.move:steps 参数控制插值步数
move(x, y, options) 派发 mousemove 事件,唯一的可选项是 steps:
steps(int):默认为 1。设为n时,Playwright 会在"当前光标位置"到目标点之间发送n个线性插值的mousemove事件;设为 1 时只在终点发出单个mousemove事件。
服务端 move 的实现 清晰地展示了插值逻辑:
const { steps = 1 } = options;
const fromX = this._x;
const fromY = this._y;
this._x = x;
this._y = y;
for (let i = 1; i <= steps; i++) {
const middleX = fromX + (x - fromX) * (i / steps);
const middleY = fromY + (y - fromY) * (i / steps);
await this._raw.move(progress, middleX, middleY, this._lastButton, this._buttons, this._keyboard._modifiers(), !!options.forClick);
}
几个从源码可以确认的事实:
- Mouse 内部维护了持久化坐标(
_x、_y字段,初始为 0,0),每次move的插值起点是上次结束位置,因此多次move调用会形成连续轨迹; - 每次
move会携带当前按住的按键集合(_buttons)和键盘修饰键状态(_keyboard._modifiers()),所以拖拽(down之后move)发出的mousemove事件带有正确的buttons标志位; steps > 1的连续移动更容易通过一些检测"瞬移式"鼠标轨迹的前端逻辑,做拖拽动画、Canvas 绘制等测试时建议使用。
Mouse.down 与 Mouse.up:手动控制按键生命周期
down(options):派发mousedown事件;up(options):派发mouseup事件。
两者的可选项相同:
| 参数 | 默认值 | 说明 |
|---|---|---|
button |
left |
鼠标按键,可选 left / middle / right |
clickCount |
1 | 对应 UIEvent.detail |
从 服务端 down/up 实现 看,down 会把指定按键加入内部 _buttons 集合并记录 _lastButton,up 则从集合中移除对应按键并把 _lastButton 复位为 'none'。这意味着多个按键可以处于同时按下的状态(例如先 down({button:'left'}) 再 down({button:'right'})),事件中的 buttons 位掩码会如实反映,这对测试多选、右键菜单与左键并发等场景很重要。
Mouse.wheel:程序化滚动页面
wheel(deltaX, deltaY) 自 v1.15 引入,派发 wheel 事件,通常用于手动滚动页面。两个参数均为 float:
deltaX:水平滚动像素数;deltaY:垂直滚动像素数。
官方文档特别注明:wheel 事件若未被页面处理可能引起滚动,且该方法不会等待滚动动画结束就返回——因此在 wheel 之后断言滚动位置前,建议配合 page.waitForFunction 或等待懒加载内容出现。
关于滚动,Playwright 的输入指南给出了一条重要原则:大多数时候 Playwright 会在动作前自动滚动元素进入视口,无需手动滚动。真正需要手动滚动的场景是触发无限列表加载、或把页面定位到特定截图位置。文档推荐的可靠做法分两档:
- 让特定元素可见(首选):
await page.getByText('Footer text').scrollIntoViewIfNeeded(); - 精确控制滚动:先把鼠标悬停到目标容器上,再使用
wheel;或者直接用evaluate修改scrollTop:
// Position the mouse and scroll with the mouse wheel
await page.getByTestId('scrolling-container').hover();
await page.mouse.wheel(0, 10);
// Alternatively, programmatically scroll a specific element
await page.getByTestId('scrolling-container').evaluate(e => e.scrollTop += 100);
注意 wheel 作用在鼠标当前悬停的位置——这就是为什么先 hover() 再 wheel():滚轮事件发送到光标所在元素,若光标停在页面根上则滚动整个页面,停在可滚动容器上则滚动容器。Chromium 的 wheel 实现 正是把当前坐标与 delta 一起通过 CDP 的 Input.dispatchMouseEvent(type: 'mouseWheel')发给浏览器。
事件派发链路:从 page.mouse 到浏览器
结合源码,一次 page.mouse.click(100, 200) 的完整调用链是:
- 客户端:client 侧 Mouse 类 把参数打包,通过
_page._channel.mouseClick(...)发送(内部使用kNoTimeout常量,不受 API 默认超时约束); - Dispatcher:pageDispatcher 收到协议消息后调用
this._page.mouse.apiWheel/apiClick/...,api*方法会先触发instrumentation.onBeforeInputAction——这就是 Trace Viewer 里红点的来源; - 服务端 Mouse:server/input.ts 中的 Mouse 类 维护坐标、按键集合、修饰键,完成插值与点击序列编排,再委托给各浏览器的
RawMouse实现; - 浏览器适配层:例如 Chromium 的 crInput 将
move/down/up/wheel分别映射为 CDP 的mouseMoved、mousePressed、mouseReleased、mouseWheel事件类型;Firefox 与 WebKit 有各自的对应实现,从而保证三套浏览器收到一致语义的鼠标事件。
从这条链路可以推断两点实践结论:其一,Mouse API 发出的是浏览器原生输入管线认可的真实输入事件(而非 el.click() 这类合成 DOM 事件),因此能触发 :active 伪类、原生拖拽、pointerdown 等依赖真实输入的行为;其二,每次输入动作都会进入 instrumentation 与 Trace 记录,输入序列在 Trace Viewer 中是可回放、可定位的。
适用场景与选型建议
- 优先用元素级 API:能用
locator.click()/locator.dblclick()就不要用page.mouse,前者自带可操作性等待、自动滚动和坐标计算; - 用 Mouse API 的典型场景:Canvas 绘图与拖拽轨迹(
down → 多次 move → up)、需要精确视口坐标的点击(点击空白区域、contextmenu右键click(x, y, { button: 'right' }))、测试滚轮驱动的分页/懒加载(hover + wheel)、以及需要"按住 N 毫秒"的长点击(click的delay或手动down/wait/up); - 参数速记:
button默认left、clickCount默认 1、delay默认 0、steps默认 1;wheel不等待滚动动画完成。
以上行为均以当前仓库源码与 Mouse 参考文档 为准,参数共享定义可查阅 docs/src/api/params.md 中的 input-button、input-click-count、input-down-up-delay、input-mousemove-steps 条目。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00