表单操作:选择/勾选/聚焦/悬停
本教程共 59 篇 · 第 18 篇 · 更新于 2026-08-04 · 约 9 分钟阅读
18. 表单操作:选择/勾选/聚焦/悬停
本节目标:能熟练操作下拉框、复选框、单选按钮,并会用聚焦和悬停触发页面的动态行为。
上一章解决了点击和输入。表单里还剩几类控件,各有各的脾气。
下拉框不能靠点击选值,复选框不能靠点击切换(至少不该靠),悬停菜单更是新手的重灾区。这章一个个拆。
下拉选择:selectOption
原生 <select> 元素用 selectOption() 操作。它有三种匹配方式:
// 按 value 或 label 匹配(传字符串时两者都试)
await page.getByLabel('选择颜色').selectOption('blue');
// 明确按 label(用户看到的文字)匹配
await page.getByLabel('选择颜色').selectOption({ label: '蓝色' });
// 明确按 value(HTML 属性值)匹配
await page.getByLabel('选择颜色').selectOption({ value: 'blue' });
// 按序号匹配,从 0 开始
await page.getByLabel('选择颜色').selectOption({ index: 2 });
多选下拉传数组,一次选多项:
await page.getByLabel('选择多个颜色').selectOption(['red', 'green', 'blue']);
想清空选择,传空数组:
await page.getByLabel('选择颜色').selectOption([]);
Tip优先用
{ label: '中文文案' }。value是开发写在 HTML 里的内部标识,改起来没心理负担;label 是用户看得见的东西,改动会走评审。测试挂在后者上更稳。
selectOption() 的可操作性检查是:可见、启用。不检查稳定和接收事件,因为它不模拟鼠标点开下拉列表,而是直接设置选中项并触发 change 事件。
自定义下拉怎么办
现在很多组件库的「下拉框」根本不是 <select>,而是一堆 <div> 拼出来的。这种情况 selectOption() 用不了,只能按真实交互来写:
// 先点开,再点选项
await page.getByRole('combobox', { name: '城市' }).click();
await page.getByRole('option', { name: '杭州' }).click();
判断方法很简单:打开开发者工具看标签名。是 <select> 就用 selectOption,不是就老老实实点两下。
勾选与取消:check / uncheck / setChecked
复选框和单选按钮用 check():
// 勾选
await page.getByLabel('我已阅读并同意条款').check();
// 取消勾选(单选按钮不支持取消)
await page.getByLabel('订阅邮件').uncheck();
// 选中一个单选按钮
await page.getByLabel('XL 码').check();
这三个方法适用于 input[type=checkbox]、input[type=radio],以及带 role="checkbox" 的自定义组件。
如果选中状态来自变量,用 setChecked() 更顺手:
const shouldSubscribe = true;
await page.getByLabel('订阅邮件').setChecked(shouldSubscribe);
它内部会判断当前状态,只在需要时才动手。
Warning别用
click()代替check()。click()是「切换」语义,跑两遍就变回去了;check()是「确保为选中」语义,跑几遍结果都一样。测试重跑、重试的时候,这个差别会救命。
check() 系列会跑完整的四项检查:可见、稳定、接收事件、启用。因为它确实模拟了一次真实点击,和 click() 同级。
顺手断言一下
改完状态最好确认一下,用专门的断言:
await page.getByLabel('订阅邮件').check();
await expect(page.getByLabel('订阅邮件')).toBeChecked();
// 断言未选中
await expect(page.getByLabel('接收广告')).not.toBeChecked();
toBeChecked() 会自动重试,不用担心状态更新有延迟。断言的完整体系在第 22 章讲。
聚焦与失焦:focus / blur
有些校验逻辑挂在 focus 或 blur 事件上。比如密码框聚焦时弹出强度提示,失焦时才校验格式。
// 让元素获得焦点
await page.getByLabel('密码').focus();
// 移除焦点
await page.getByLabel('密码').blur();
这两个方法不做任何可操作性检查。它们直接调 DOM 的 focus() / blur(),元素被遮挡也照样生效。
典型用法是测「离开输入框才报错」这种场景:
await page.getByLabel('邮箱').fill('not-an-email');
await page.getByLabel('邮箱').blur();
await expect(page.getByText('邮箱格式不正确')).toBeVisible();
想确认焦点落在哪,用 toBeFocused():
await expect(page.getByLabel('用户名')).toBeFocused();
悬停:hover
鼠标移上去才展开的菜单、才显示的提示气泡,都靠 hover():
await page.getByRole('button', { name: '更多' }).hover();
await page.getByRole('menuitem', { name: '删除' }).click();
hover() 检查可见、稳定、接收事件,但不检查启用。禁用的元素也能悬停,这符合真实情况。
悬停是新手翻车最多的地方,翻车原因就两个。
一是菜单收得太快。 有些实现是鼠标一离开就收起,而 Playwright 移向下一个元素的路上正好触发了收起。先确认菜单项是不是宿主的子元素:是的话鼠标路径不会离开父容器,没问题;不是的话,考虑改用键盘导航。
二是悬停完没等展开。 hover() 之后直接 click(),菜单可能还在播展开动画。click() 的「稳定」检查通常能帮你兜住。真不放心就先加一条断言:
await page.getByRole('button', { name: '更多' }).hover();
await expect(page.getByRole('menu')).toBeVisible();
await page.getByRole('menuitem', { name: '删除' }).click();
各操作检查项对照
把这两章的方法放一起对比,心里更有数:
| 操作 | 可见 | 稳定 | 接收事件 | 启用 | 可编辑 |
|---|---|---|---|---|---|
click() / dblclick() | 是 | 是 | 是 | 是 | - |
check() / uncheck() / setChecked() | 是 | 是 | 是 | 是 | - |
hover() | 是 | 是 | 是 | - | - |
fill() / clear() | 是 | - | - | 是 | 是 |
selectOption() | 是 | - | - | 是 | - |
focus() / blur() | - | - | - | - | - |
press() / pressSequentially() | - | - | - | - | - |
表里的「可编辑」是指:元素启用且没有 readonly 属性。所以只读输入框上 fill() 会超时失败,focus() 却照样能用。
一个完整表单示例
import { test, expect } from '@playwright/test';
test('提交注册表单', async ({ page }) => {
await page.goto('https://example.com/signup');
await page.getByLabel('用户名').fill('mashangxue');
await page.getByLabel('城市').selectOption({ label: '杭州' });
await page.getByLabel('我已阅读并同意条款').check();
// 失焦触发校验
await page.getByLabel('邮箱').fill('me@example.com');
await page.getByLabel('邮箱').blur();
await expect(page.getByLabel('我已阅读并同意条款')).toBeChecked();
await page.getByRole('button', { name: '注册' }).click();
await expect(page).toHaveURL(/\/welcome/);
});
小结
- 原生
<select>用selectOption(),优先按 label 匹配;自定义下拉靠点击。 - 勾选用
check()/uncheck(),状态来自变量时用setChecked(),别用click()切换。 focus()/blur()不做检查,专门用来触发焦点相关逻辑。hover()用于悬停菜单,配合断言确认菜单已展开更稳。- 记住各操作的检查项差异,能帮你快速定位「为什么这里报超时」。
下一章预告:键盘与鼠标高级操作——按键、组合键、坐标级鼠标控制和拖拽,高层 API 搞不定时往下降一层。