最近不少做Web自动化测试的朋友都在聊Playwright,尤其是Java技术栈的同学,上手之后普遍反馈“比老牌工具清爽很多”。之前写了这个系列的上篇,讲了环境搭建和浏览器基础操作,这次专门把表单里最常用的两类控件——单选按钮(Radio Button)和多选按钮(Checkbox)拿出来细讲。别小看这两个小控件,实际项目里踩坑率一点不低,光是“看起来点上了,断言却没通过”的情况我就排查过好多次。
这篇中篇教程会从定位策略讲起,再到选中、取消、状态断言,最后整理我实际遇到的几个典型问题。无论你是刚接触Playwright的新手,还是已经在项目里用它写了不少用例的老手,只要被表单元素折腾过,这篇的内容应该都能帮你省点时间。
1. 环境准备与基础认知
开始操作控件之前,先把基础环境弄利索。很多人喜欢直接开写用例,结果跑起来报错才回头补环境,反而耽误时间。我这里基于Java 11 + Playwright最新稳定版来讲,构建工具用Maven。
1.1 在pom.xml中引入Playwright依赖
Maven项目里引入Playwright非常简单,只需要在pom.xml中添加依赖,Maven会自动拉取对应平台的浏览器驱动包。
<dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>1.45.0</version> </dependency>版本号建议用当前最新稳定版,可以去中央仓库查一下。引入依赖之后执行mvn clean install,确认依赖下载成功。有些网络环境下载慢,多试几次或者换个镜像源一般就能解决。
1.2 安装浏览器内核
这一步很多人会忽略,只引入Java依赖还不够,Playwright需要有浏览器内核才能工作。命令行执行:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium"如果你还需要Firefox或WebKit,把命令里的chromium换成对应名称即可。我个人建议日常开发用Chromium就够了,跨浏览器兼容性测试再补装其他的。用公司内部测试环境的同学注意,如果网络受限导致安装失败,可以用PLAYWRIGHT_DOWNLOAD_HOST环境变量指定镜像地址,具体配置我这里不多说,网上有现成方案。
1.3 一个关键认知:Playwright的自动等待机制
写控件操作之前,我觉得有必要先讲清楚Playwright和传统自动化工具的一个核心差异——自动等待。这个机制直接影响你后续写代码的思路。
老牌工具里,最常见的写法是“找到元素——判断可见——点击”,每一步之间还要手动加Thread.sleep或者显式等待。Playwright的思路完全不同,它的定位器(Locator)是惰性的,你调用locator.click()或者locator.check()时,它会自动等待元素出现在DOM中、可见、可交互,然后才执行操作。默认超时时间是30秒,也可以按需调整。
// 不需要手动sleep,Playwright会自动等待 page.locator("#submitBtn").click();这个机制让代码简洁了很多,但也带来一个容易踩的坑:如果你在页面还没跳转时就调用操作,Playwright会一直等到超时才抛错。所以前面几篇提到的“理解页面生命周期”还是很重要的。
2. 单选按钮核心操作:定位、选择与状态判定
单选按钮在表单里很常见,性别选择、套餐类型、支付方式……看起来简单,实际操作的时候有几个细节不当心就会翻车。
2.1 先搞清楚Radio Button的天然分组特性
HTML里的一组单选按钮,靠的是同名name属性来分组:
<input type="radio" name="deliveryType" value="express" /> 顺丰速运 <input type="radio" name="deliveryType" value="standard" /> 标准快递 <input type="radio" name="deliveryType" value="pickup" /> 到店自取同一组里name相同,用户只能选择其中一个。选了一个,其他就自动取消选中。这是浏览器原生行为,但很多测试新手容易忽略这个特性,写用例时习惯把三个按钮当独立控件处理,结果断言的时候发现状态互相影响,一脸懵。
Playwright处理这种场景很方便,你可以用page.getByName("deliveryType")获取同组所有按钮,也可以直接根据值定位具体某一个。
2.2 三种常用定位策略对比
Playwright里定位元素的方式很丰富,单选按钮常用的有三种:
第一种,通过getByLabel关联文本定位。这种最贴近用户视角,推荐优先使用。
<label><input type="radio" name="deliveryType" value="express" /> 顺丰速运</label>Locator expressRadio = page.getByLabel("顺丰速运");第二种,通过CSS选择器定位。
Locator expressRadio = page.locator("input[type='radio'][value='express']");第三种,通过getByRole定位,符合无障碍语义,也是官方推荐的做法。
Locator expressRadio = page.getByRole(AriaRole.RADIO, new Page.GetByRoleOptions().setName("顺丰速运"));定位策略本身没有绝对好坏,关键看页面结构。如果label和input是包裹关系,getByLabel最省心;如果DOM结构比较复杂,那就需要用CSS配合nth或者filter来精确锁定。我个人的经验是第一选择先试getByRole或者getByLabel,实在不行再退回CSS。
2.3 选择操作:check方法与setChecked方法
Playwright为单选按钮提供了专门的check()方法,这个方法会确保元素最终处于选中状态。
Locator expressRadio = page.getByLabel("顺丰速运"); expressRadio.check();执行完之后,Playwright会内部校验isChecked()是否为true,如果校验失败会抛异常。所以你不一定需要立即写断言,框架层面已经做了一层保障。
还有setChecked(true或false)方法,从名字就能看出来,它显式指定要设置的状态。注意,单选按钮分组内只能选一个,如果你对一个已经在选中状态的radio调用setChecked(false),Playwright会报错,因为radio不支持直接取消选中——这是HTML本身的限制,不是你写错了。多选按钮就没有这个限制。
注意,单选按钮不能被直接取消,只能通过选中同组的另一个按钮来切换。别在这上面纠结。
2.4 为什么明明click()也能选中,却不推荐这么写
有些习惯了老工具的同学可能会问:那我直接click()不也一样能选中吗?确实能。但check()相比click()有两点好处。
第一,check()只会在未选中状态下执行点击,如果当前已经是选中状态,它就不会再做无意义的点击。这能减少页面操作的副作用。第二,check()在点击之后会自动验证选中状态,如果点击没生效(比如按钮被遮挡、disabled),它会明确报错,而不是让问题悄悄埋到下一步的断言里。
当然,如果你的目标恰好是“点击这个交互元素”而不是“确保选中”,比如要测试某个按钮是否有响应事件,那用click()是合理的。测试意图不同,选用的方法就应该不同。
3. 多选按钮批量处理:全选、反选、取消勾选
多选按钮的使用场景比单选还多,权限分配、兴趣爱好、商品标签、筛选条件……而且往往以列表形式出现,一操作就是一整组。
3.1 Checkbox与Radio的本质差异
checkbox是多选控件,同一组内可以同时勾选多个。和radio还有一个区别是,checkbox允许直接取消选中,所以你的测试用例里会覆盖“勾选-取消-再勾选”这类场景。
HTML结构长这样:
<input type="checkbox" name="hobby" value="reading" /> 阅读 <input type="checkbox" name="hobby" value="sports" /> 运动 <input type="checkbox" name="hobby" value="music" /> 音乐操作方式和单选类似,check()和uncheck()都可以用。
Locator reading = page.getByLabel("阅读"); Locator sports = page.getByLabel("运动"); reading.check(); sports.check(); sports.uncheck();3.2 批量全选与取消的省力写法
测试中的常见场景是先全选,再验证提交结果,最后全部取消。如果一个个去点每个checkbox,代码会非常啰嗦。Playwright的定位器支持.all()方法,可以直接拿到所有匹配元素。
// 勾选所有勾选框 Locator hobbyCheckboxes = page.locator("input[type='checkbox']"); for (Locator cb : hobbyCheckboxes.all()) { if (!cb.isChecked()) { cb.check(); } }不过这种写法有个前提:这些checkbox必须都在同一个页面或者同一个可见容器内。如果页面有分页或者懒加载,all()只能拿到当前DOM中的元素,这点要注意。
如果要取消全部勾选,把判断反过来即可。
for (Locator cb : hobbyCheckboxes.all()) { if (cb.isChecked()) { cb.uncheck(); } }3.3 按条件筛选后勾选:filter和and组合
很多实际场景不是无脑全选,而是根据条件筛选后操作。比如一个用户权限页面,有系统管理、订单管理、商品管理等一组权限,每个权限下还有查看、编辑、删除三个checkbox。我要勾选“订单管理”分组下的“编辑”权限。
这种需求用locator.filter()可以轻松实现。
Locator orderEditCb = page.locator(".permission-item") .filter(new Locator.FilterOptions().setHasText("订单管理")) .locator("input[value='edit']"); orderEditCb.check();先用filter过滤出包含“订单管理”文本的容器,再在这个容器范围内定位input[value='edit']。这个写法比你从头写一个长CSS选择器要清晰得多,页面结构变化时维护成本也低。
你还可以用and()方法组合条件:
Locator exactCb = page.getByRole(AriaRole.CHECKBOX) .and(page.locator("input[value='edit']"));3.4 动态列表场景下的处理思路
还有一种常见场景是编辑页面里有一个动态添加标签的功能,用户点“添加”就会多一行checkbox,行的总数不定。对这种场景,不要指望用固定索引去定位,因为列表顺序随时会变。比较稳妥的做法是先拿到当前列表的数量,遍历操作,并且每次操作后更新数量。
// 先统计当前checkbox数量 int total = page.locator(".tag-list input[type='checkbox']").count(); for (int i = 0; i < total; i++) { page.locator(".tag-list input[type='checkbox']").nth(i).check(); }这里要注意count是在循环开始前确定的,如果你在循环内部添加或删除元素,会导致实际数量和缓存值不一致。我的习惯是能不动态改列表就不改,如果必须改,每次重新count。
4. 状态校验与测试断言技巧
操作做完了,断言是最后一道关口。很多用例挂掉不是操作问题,而是断言写得不够严谨。
4.1 isChecked方法:直接判断勾选状态
Playwright的isChecked()方法可以直接判断当前元素是否处于选中状态。
if (page.getByLabel("顺丰速运").isChecked()) { System.out.println("当前选中了顺丰速运"); }这个方法返回布尔值,不会等待重试。所以如果你要验证“点击后是否选中”,建议配合等待机制。但要注意,isChecked()和isVisible()一样,只能反映调用时刻的状态,如果你需要“等待某个状态成立”,应该用assertThat配合轮询。
4.2 用assertThat做自动等待断言
Playwright提供了断言封装,能自动等待条件满足,这也是它比单纯拿布尔值判断更可靠的地方。
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; // 自动等待直到选中状态成立 assertThat(page.getByLabel("顺丰速运")).isChecked();这个断言默认也是等待30秒,适合用在操作之后校验状态变化。如果条件不满足,它会抛出一个包含定位器和实际状态的详细错误信息,排查起来直观很多。
写用例的时候记住一个原则:能用assertThat的就别用if + isChecked。前者有自动重试,后者只是一次性快照。
4.3 用参数化让数据驱动更优雅
单选和多选按钮的测试用例,数据组合往往不少。如果每个用例都写一遍完整逻辑,代码会冗余得没法维护。我一般会把测试数据抽出来,用参数化方式驱动。
@DataProvider(name = "deliveryOptions") public Object[][] deliveryOptions() { return new Object[][] { {"顺丰速运", "express"}, {"标准快递", "standard"}, {"到店自取", "pickup"} }; } @Test(dataProvider = "deliveryOptions") public void testSelectDeliveryType(String label, String expectedValue) { page.getByLabel(label).check(); assertThat(page.locator("input[name='deliveryType']:checked")).hasValue(expectedValue); }这样新增一个配送方式只需要在数据组里加一行,用例代码完全不用动。数据驱动的好处是测试意图清晰,后续维护成本低,强烈推荐在大量表单控件测试中使用。
5. 常见问题与排查实录
这一部分总结我在实际项目中遇到过的坑,每一个都是真实排查过的,希望能帮你提前避开。
5.1 元素不可见导致操作失败
先看一个典型的报错信息:
Locator.check: Timeout 30000ms exceeded. Call log: - waiting for locator("input[type=radio]").nth(2)这个报错看起来是元素找不到,但很多时候真正的问题是元素压根不可见。比如页面上的单选按钮使用了自定义渲染,原生input被隐藏起来,只显示一个div模拟的按钮。Playwright的自动等待只对可见元素有效,隐藏元素它不会强行点击。
排查方法很简单,打开DevTools检查一下目标元素是不是display:none或者visibility:hidden。如果是,你就不能用check(),而应该通过点击label或者外层容器来触发选中。这也是为什么前面我反复强调优先用getByLabel——label是可见的,点击label原生就会触发radio选中。
5.2 点击label和点击input的区别
在上面所说的隐藏input场景里,直接page.getByLabel("顺丰速运").click()其实也能生效,因为点击label等价于点击关联的input。但如果你的页面结构不是label包裹input,而是for属性关联,那就要确认for指向的id是否正确。有些前端框架动态渲染时id会带随机后缀,导致for指向失效。
我的做法是:优先getByLabel,失败后直接在浏览器里确认关联关系,再选择合适的locator。
5.3 Strict模式拦截:多个匹配元素报错
Playwright的定位器是严格模式,如果你定位的元素在页面上有多个匹配,它会直接报错,不会猜测你要操作哪个。这个设计很合理,但第一次遇到的人会觉得奇怪。
Locator.check: locator("input[value='express']") resolved to 5 elements原因很简单——页面上不同表单区域有多个相同value的radio。解决方法是缩小定位范围,先定位到具体表单容器,再在该容器下定位。
Locator formContainer = page.locator("#orderForm"); formContainer.locator("input[value='express']").check();5.4 页面重渲染导致引用失效
单页应用里,某个操作可能触发Vue或React重新渲染,之前获取的元素引用变成“悬挂”状态。Playwright的Locator是惰性的,每次操作都会重新查询DOM,所以大部分情况下不会有问题。但如果你在循环里反复用一个Locator对象,并且循环过程中页面发生了重渲染,仍有可能遇到元素detached的报错。
Element is not attached to the DOM这种情况下,每次循环重新获取Locator即可,不要长时间持有引用。
5.5 快速排查清单
我把上面提到的问题整理成一张速查表,方便你遇到问题的时候快速对照:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| check()超时 | 元素不可见或disabled | 检查CSS、是否有遮罩层 |
| 定位到多个元素 | 选择器匹配范围过大 | 缩小到表单容器内定位 |
| 点击无效但没报错 | label和input关联断裂 | 检查label的for属性是否正确指向 |
| 断言失败但操作成功 | 页面重渲染导致状态校验过早 | 改用assertThat自动等待 |
| 同一个radio无法取消 | 单选按钮本身不支持直接取消 | 选中同组另一个radio或刷新页面 |
排查的时候我习惯先从元素可见性入手,因为大部分诡异问题最后都跟元素当面状态有关。打开DevTools,确认元素可见、可用、没有被覆盖,再去看代码逻辑。
6. 实操心得与扩展建议
整个中篇写下来,我个人最想强调的一点是:不要硬记API,而是理解控件背后的HTML语义,再想清楚自动化工具的操作到底触发了什么浏览器行为。单选按钮天然的分组约束、checkbox的独立互不干扰、隐藏input时点击label的触发链路,这些底层逻辑一旦通了,写用例就顺畅多了。
还有一个建议是尽量把你项目里常用的表单控件封装成通用方法,比如selectRadioByLabel(page, groupName, label)、batchCheck(page, groupName, List<String> labels)。封装以后用例代码会变得非常短,可读性也高很多。本系列的下篇我打算继续聊下拉框的定位与操作,包括原生select和自定义下拉两种场景,顺便讲讲怎么处理联动下拉这种高复杂度交互。
最后再分享一个我在真实项目里反复验证过的小技巧:凡是页面里有动画或者过渡效果的元素,操作之后不要立刻断言,用assertThat的自动等待功能,让结果稳定之后再校验。这个习惯能帮你省掉无数条看似随机失败的用例。