Playwright 操作(Actions)
本章介绍 Playwright 中与导航和点击相关的操作,以及操作背后的自动等待机制。
什么是 Action
Action(操作)是 Playwright 中用来模拟用户行为的方法,比如点击按钮、输入文字、选择下拉框。
每个 Action 在执行之前,Playwright 会自动等待目标元素变为可操作状态。
Actionability 可操作性检查
在执行每个操作前,Playwright 会对目标元素进行一系列检查:
| 步骤 | 检查条件 | 说明 |
|---|---|---|
| 1. Attached | 元素已挂载到 DOM 树 | 元素存在于页面中 |
| 2. Visible | 元素可见(非 display:none 或 visibility:hidden) | 用户能看见元素 |
| 3. Stable | 元素位置稳定(动画结束) | 避免动画期间的误操作 |
| 4. Receives Events | 元素没有被其他元素遮挡 | 确保点击能被元素接收 |
| 5. Enabled | 元素未被禁用(非 disabled) | 用户可以与之交互 |
任意一步未通过,Playwright 都会等待并重试,直到超时。
这 5 步检查是 Playwright 自动完成的,你不需要写任何额外的等待代码。这也是 Playwright 能够大幅减少不稳定测试的根本原因。
强制操作 { force: true }
有时你需要绕过可操作性检查,例如点击一个被遮罩隐藏的按钮或测试不可见元素的状态。
实例
// 强制点击,跳过所有可操作性检查
await page.getByRole('button', { name: '提交' }).click({ force: true });
await page.getByRole('button', { name: '提交' }).click({ force: true });
force: true是测试真实用户做不到的操作,绝大多数情况下不应使用。如果你的测试需要force: true,先检查是否有交互设计问题。
导航操作
page.goto() 页面跳转
实例
// 基本导航
await page.goto('https://www.runoob.com/');
// 等待 DOMContentLoaded(页面结构加载完成)
await page.goto('https://www.runoob.com/', { waitUntil: 'domcontentloaded' });
// 等待网络空闲(所有异步数据加载完毕)
await page.goto('https://www.runoob.com/', {
waitUntil: 'networkidle', // 网络空闲
timeout: 60000, // 超时 60 秒
referer: 'https://google.com', // Referer 来源
});
await page.goto('https://www.runoob.com/');
// 等待 DOMContentLoaded(页面结构加载完成)
await page.goto('https://www.runoob.com/', { waitUntil: 'domcontentloaded' });
// 等待网络空闲(所有异步数据加载完毕)
await page.goto('https://www.runoob.com/', {
waitUntil: 'networkidle', // 网络空闲
timeout: 60000, // 超时 60 秒
referer: 'https://google.com', // Referer 来源
});
page.goBack() / goForward() / reload()
实例
// 后退一页
await page.goBack();
// 前进一页
await page.goForward();
// 刷新页面
await page.reload();
await page.goBack();
// 前进一页
await page.goForward();
// 刷新页面
await page.reload();
点击操作
locator.click() 单击
实例
// 基本单击
await page.getByRole('button', { name: '登录' }).click();
// 带选项的点击
await page.getByRole('button', { name: '登录' }).click({
button: 'left', // 鼠标按键:'left' | 'right' | 'middle'
clickCount: 1, // 点击次数
delay: 100, // 按下与松开之间的延迟(毫秒)
timeout: 5000, // 超时时间
modifiers: [], // 修饰键:['Alt', 'Control', 'Meta', 'Shift']
});
await page.getByRole('button', { name: '登录' }).click();
// 带选项的点击
await page.getByRole('button', { name: '登录' }).click({
button: 'left', // 鼠标按键:'left' | 'right' | 'middle'
clickCount: 1, // 点击次数
delay: 100, // 按下与松开之间的延迟(毫秒)
timeout: 5000, // 超时时间
modifiers: [], // 修饰键:['Alt', 'Control', 'Meta', 'Shift']
});
locator.dblclick() 双击
实例
// 双击元素
await page.getByText('双击编辑').dblclick();
await page.getByText('双击编辑').dblclick();
右键点击
实例
// 右键点击(打开上下文菜单)
await page.getByText('右键菜单').click({ button: 'right' });
await page.getByText('右键菜单').click({ button: 'right' });
组合键点击
实例
// Ctrl + 点击(多选)
await page.getByRole('option').click({ modifiers: ['Control'] });
// Shift + 点击(范围选择 / 新窗口打开)
await page.getByRole('link').click({ modifiers: ['Shift'] });
await page.getByRole('option').click({ modifiers: ['Control'] });
// Shift + 点击(范围选择 / 新窗口打开)
await page.getByRole('link').click({ modifiers: ['Shift'] });
坐标点击
实例
// 点击元素的特定位置(相对于元素左上角)
await page.locator('canvas').click({
position: { x: 100, y: 50 },
});
await page.locator('canvas').click({
position: { x: 100, y: 50 },
});
locator.hover() 悬停
实例
// 鼠标悬停(触发 hover 效果或下拉菜单)
await page.getByRole('link', { name: '教程' }).hover();
// 悬停后点击弹出的子菜单
await page.getByRole('link', { name: 'Playwright 教程' }).click();
await page.getByRole('link', { name: '教程' }).hover();
// 悬停后点击弹出的子菜单
await page.getByRole('link', { name: 'Playwright 教程' }).click();
点击 vs hover 的场景差异
| 操作 | 适用场景 |
|---|---|
click() | 按钮点击、链接跳转、复选框切换 |
dblclick() | 编辑模式进入、双击选择文本 |
click({ button: 'right' }) | 打开浏览器原生上下文菜单 |
hover() | 触发悬停菜单、Tooltip 提示 |
click({ position }) | Canvas 操作、自定义图形交互 |
文本输入操作
locator.fill() 清空后填入
fill() 会先清空输入框,再填入新值,是输入操作中最常用和最推荐的方法。
实例
// 清空输入框并填入新值
await page.getByLabel('用户名').fill('runoob_user');
await page.getByLabel('密码').fill('secure_password');
await page.getByPlaceholder('搜索RUNOOB教程').fill('Playwright');
await page.getByLabel('用户名').fill('runoob_user');
await page.getByLabel('密码').fill('secure_password');
await page.getByPlaceholder('搜索RUNOOB教程').fill('Playwright');
locator.type() 逐字输入
type() 会逐个字符输入,模拟真实的键盘敲击,每次按键之间可设置间隔。
实例
// 逐字输入(每个字符间有 100ms 延迟)
await page.getByLabel('搜索').type('Playwright', { delay: 100 });
await page.getByLabel('搜索').type('Playwright', { delay: 100 });
fill vs type 的区别
| 方法 | 行为 | 适用场景 |
|---|---|---|
fill() | 清空后一次性填入 | 绝大多数场景(推荐) |
type() | 逐字符输入,触发每次按键事件 | 需要触发输入联想、实时搜索 |
大部分情况下使用
fill()即可。只有当输入框有实时搜索(keydown/keyup 事件)时,才需要使用type()。
locator.clear() 清空
实例
// 清空输入框内容
await page.getByLabel('用户名').clear();
await page.getByLabel('用户名').clear();
选择操作
locator.check() / uncheck() 复选框
实例
// 勾选复选框
await page.getByLabel('我同意服务协议').check();
// 取消勾选
await page.getByLabel('接收邮件通知').uncheck();
// 设置勾选状态
await page.getByLabel('记住我').setChecked(true); // 确认为勾选
await page.getByLabel('记住我').setChecked(false); // 确认为未勾选
await page.getByLabel('我同意服务协议').check();
// 取消勾选
await page.getByLabel('接收邮件通知').uncheck();
// 设置勾选状态
await page.getByLabel('记住我').setChecked(true); // 确认为勾选
await page.getByLabel('记住我').setChecked(false); // 确认为未勾选
locator.selectOption() 下拉框选择
实例
// 通过 value 属性选择
await page.getByLabel('城市').selectOption('beijing');
// 通过显示文本选择
await page.getByLabel('城市').selectOption({ label: '上海' });
// 多选下拉框
await page.getByLabel('兴趣标签').selectOption([
{ label: '编程' },
{ value: 'design' },
]);
// 通过索引选择(第 0 项、第 2 项)
await page.getByLabel('城市').selectOption({ index: 0 });
await page.getByLabel('城市').selectOption('beijing');
// 通过显示文本选择
await page.getByLabel('城市').selectOption({ label: '上海' });
// 多选下拉框
await page.getByLabel('兴趣标签').selectOption([
{ label: '编程' },
{ value: 'design' },
]);
// 通过索引选择(第 0 项、第 2 项)
await page.getByLabel('城市').selectOption({ index: 0 });
键盘操作
page.keyboard.press() 按键
实例
// 按下并释放 Enter
await page.keyboard.press('Enter');
// 全选(Control + A)
await page.keyboard.press('Control+A');
// 复制粘贴
await page.keyboard.press('Control+C');
await page.keyboard.press('Control+V');
// 撤销
await page.keyboard.press('Control+Z');
await page.keyboard.press('Enter');
// 全选(Control + A)
await page.keyboard.press('Control+A');
// 复制粘贴
await page.keyboard.press('Control+C');
await page.keyboard.press('Control+V');
// 撤销
await page.keyboard.press('Control+Z');
page.keyboard.down() / up() 按下/松开
实例
// 按住 Shift 不松开
await page.keyboard.down('Shift');
// 在此期间按其他键(相当于按住 Shift 再按其他键)
await page.keyboard.press('ArrowRight'); // Shift + 右箭头(选中文本)
await page.keyboard.press('ArrowRight');
// 松开 Shift
await page.keyboard.up('Shift');
await page.keyboard.down('Shift');
// 在此期间按其他键(相当于按住 Shift 再按其他键)
await page.keyboard.press('ArrowRight'); // Shift + 右箭头(选中文本)
await page.keyboard.press('ArrowRight');
// 松开 Shift
await page.keyboard.up('Shift');
特殊键名称
| 键名 | 对应按键 |
|---|---|
Enter | 回车 |
Escape | Esc 退出 |
Tab | Tab 切换焦点 |
Backspace | 退格删除 |
Delete | 删除键 |
ArrowUp/Down/Left/Right | 方向键 |
PageUp/PageDown | 翻页键 |
Home/End | 行首/行尾 |
Control/Alt/Shift/Meta | 修饰键 |
page.keyboard.type() 键盘输入文本
实例
// 在焦点元素上输入文本
await page.keyboard.type('RUNOOB 教程', { delay: 50 });
await page.keyboard.type('RUNOOB 教程', { delay: 50 });
page.keyboard.insertText() 直接插入
实例
// 在焦点位置直接插入文本(不模拟键盘事件,速度最快)
await page.keyboard.insertText('快速插入的文本');
await page.keyboard.insertText('快速插入的文本');
鼠标操作
坐标点击与移动
实例
// 在视口坐标 (100, 200) 处点击
await page.mouse.click(100, 200);
// 在坐标处双击
await page.mouse.dblclick(100, 200);
// 移动鼠标到坐标
await page.mouse.move(100, 200);
// 按下 / 松开
await page.mouse.down();
await page.mouse.up();
await page.mouse.click(100, 200);
// 在坐标处双击
await page.mouse.dblclick(100, 200);
// 移动鼠标到坐标
await page.mouse.move(100, 200);
// 按下 / 松开
await page.mouse.down();
await page.mouse.up();
鼠标滚轮
实例
// 向下滚动 500px
await page.mouse.wheel(0, 500);
// 向左滚动 200px
await page.mouse.wheel(200, 0);
await page.mouse.wheel(0, 500);
// 向左滚动 200px
await page.mouse.wheel(200, 0);
locator.dragTo() 拖拽
实例
// 把元素 A 拖拽到元素 B
const source = page.getByText('拖拽我');
const target = page.getByTestId('drop-zone');
await source.dragTo(target);
// 带选项的拖拽
await source.dragTo(target, {
sourcePosition: { x: 0, y: 0 }, // 拖拽起始点(相对于源元素)
targetPosition: { x: 10, y: 10 }, // 拖拽目标点(相对于目标元素)
});
const source = page.getByText('拖拽我');
const target = page.getByTestId('drop-zone');
await source.dragTo(target);
// 带选项的拖拽
await source.dragTo(target, {
sourcePosition: { x: 0, y: 0 }, // 拖拽起始点(相对于源元素)
targetPosition: { x: 10, y: 10 }, // 拖拽目标点(相对于目标元素)
});
page.mouse vs locator.click 的选择
| 方式 | 适用场景 |
|---|---|
locator.click() | 点击页面元素(绝大多数场景) |
page.mouse.click() | 点击特定坐标(Canvas、图表等无 DOM 的场景) |
文件上传
locator.setInputFiles() 单文件/多文件
实例
// 上传单个文件
await page
.getByLabel('选择文件')
.setInputFiles('path/to/document.pdf');
// 上传多个文件
await page
.getByLabel('选择文件')
.setInputFiles([
'path/to/file1.png',
'path/to/file2.png',
]);
// 清除已选文件
await page.getByLabel('选择文件').setInputFiles([]);
await page
.getByLabel('选择文件')
.setInputFiles('path/to/document.pdf');
// 上传多个文件
await page
.getByLabel('选择文件')
.setInputFiles([
'path/to/file1.png',
'path/to/file2.png',
]);
// 清除已选文件
await page.getByLabel('选择文件').setInputFiles([]);
文件路径是相对于当前工作目录(项目根目录)的路径。
文件下载
实例
// 开始等待下载事件(必须在触发下载的操作之前调用)
const downloadPromise = page.waitForEvent('download');
// 执行触发下载的操作
await page.getByRole('button', { name: '下载报告' }).click();
// 等待下载完成
const download = await downloadPromise;
// 获取建议的文件名
console.log(download.suggestedFilename());
// 保存到指定路径
await download.saveAs('downloads/report.pdf');
const downloadPromise = page.waitForEvent('download');
// 执行触发下载的操作
await page.getByRole('button', { name: '下载报告' }).click();
// 等待下载完成
const download = await downloadPromise;
// 获取建议的文件名
console.log(download.suggestedFilename());
// 保存到指定路径
await download.saveAs('downloads/report.pdf');
