最近在搞自动化测试用Java配合Playwright操作页面上的单选和多选按钮踩了一堆坑也总结了不少经验。这篇笔记是《刚刚问世》系列的开篇专门讲讲如何用Playwright可靠地点击和验证这类控件。如果你是刚入门Java自动化、或者从Selenium切换过来的测试工程师这篇教程应该能帮你少走弯路。说得直白点单选按钮和多选按钮看着简单但真正跑自动化的时候定位、点击、断言每一步都有讲究搞不好就是“假成功”——测试明明通过了实际上元素根本没选上。1. 项目背景与整体思路1.1 为什么在Java里选Playwright而不是Selenium先说结论Playwright在同步等待、元素交互、浏览器上下文管理上比Selenium顺手太多。早年用Selenium写Java用例最头疼的就是Thread.sleep()满天飞页面加载快了慢了都会导致脚本时好时坏。Playwright核心卖点是自动等待比如点击一个元素前它会自动等元素可交互基本不用手动sleep。这一点对单选多选按钮特别友好因为这类控件经常由JavaScript动态渲染稍不留神就会点到还没生成好的元素。再一个就是API设计。Java版的Playwright API与官方保持一致locator对象非常强大支持CSS、XPath、文本、属性等定位方式。尤其是locator.check()和locator.uncheck()这两个方法天生就是为单选框和复选框准备的不用我们再费劲去模拟点击或写JavaScript脚本绕路。相比之下Selenium里需要用click()配上一大堆等元素可见可点的封装代码量直接翻倍。1.2 单选和多选按钮在自动化里的真正难点很多自动化测试刚上手时会以为单选框/复选框就是“找到元素点一下”的事实际跑起来才知道有多坑。先说单选按钮同一组Radio按钮在HTML里通常共享一个name属性但Value不同。如果你不小心用XPath选到了多个元素Playwright会直接报错提醒你locator不唯一这其实是保护机制但对新手就是一头雾水。多选按钮的坑更多。复选框有三种状态选中、未选中、半选中indeterminate。半选中状态常见于“全选”和“子项”的联动场景普通点击只能切换未选中到选中根本点不出半选状态断言时也会一脸懵。另外很多页面会隐藏原生复选框改用自定义样式的span或div直接用check()可能点了没反应必须用force或先定位到父级元素。所以整个教程的核心思路就是先搞清楚控件的DOM结构再选对定位策略最后用Playwright专门的方法做交互和断言每一步都加上充分等待尽量杜绝“偶发性失败”。2. 环境准备与基础配置2.1 搭建最小可用的JavaPlaywright项目我假设你已经装了JDK 11以上和Maven没装的话先去搞定这些基础工具。项目结构用最普通的Maven工程就行不用IDE的额外魔法。在pom.xml里加入Playwright依赖版本号建议选当前最新的稳定版。我的环境用的版本是1.40左右后续升级主版本时注意API可能有细微变化。dependency groupIdcom.microsoft.playwright/groupId artifactIdplaywright/artifactId version1.40.0/version /dependency dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.8.0/version /dependency注意光是加依赖还不够。Playwright需要下载浏览器驱动运行一条命令行命令就能搞定mvn exec:java -e -D exec.mainClasscom.microsoft.playwright.CLI -D exec.argsinstall chromium如果这步下载慢把Chromium换成带系统依赖的版本或者优先使用现有的Chrome通道后面我会在常见问题里细讲。2.2 初始化浏览器和页面上下文写代码之前先建立一套基础封装。Playwright的设计是“浏览器实例Browser 上下文BrowserContext 页面Page”三层。上下文相当于一个独立的会话隔离cookie和缓存特别适合并行执行测试用例。先看一个最简单的初始化import com.microsoft.playwright.*; public class RadioCheckboxDemo { private Playwright playwright; private Browser browser; private BrowserContext context; private Page page; public void setup() { playwright Playwright.create(); browser playwright.chromium().launch(new BrowserType.LaunchOptions() .setHeadless(false) // 调试时先不隐藏浏览器跑稳定了再改true .setSlowMo(300)); // 慢动作300ms方便肉眼观察 context browser.newContext(); page context.newPage(); } public void teardown() { browser.close(); playwright.close(); } }setHeadless(false)在早期调试特别关键因为单选和多选按钮的视觉反馈很直观你一眼就能看出到底有没有被点上。setSlowMo也是调试利器相当于给每一步操作加了慢放镜头。等脚本完全稳定之后再把headless改回true放到CI里跑。3. 单选按钮操作详解3.1 精确定位Radio按钮的几个姿势说一组数据自己写用例时感受最深CSS选择器定位单选按钮成功率几乎百分百XPath偶尔会踩到动态属性变换。我习惯优先用CSS结合属性选择器。比如页面上有一组性别单选按钮DOM长这样label input typeradio namegender valuemale 男 /label label input typeradio namegender valuefemale 女 /label在Playwright里可以这样定位并勾选“男”Locator maleRadio page.locator(input[namegender][valuemale]); maleRadio.check();check()方法模拟鼠标勾选操作会自动等元素可点击。还有一点值得说check()和click()不同check()会严格校验元素类型和可见性如果元素不存在或者被禁用会给出清晰异常这对后期排查问题很有帮助。如果页面是自定义样式的Radio比如原生input被css隐藏外面包了一层div那就要先找到这个能点击的可视元素。最稳的办法是先用isVisible()确认原生input不可见时就改用locator(div.custom-radio).click()然后用原生input的选中状态做断言。这里要注意裸用check()可能报“element is not visible”的错。3.2 验证单选按钮是否真的被选中勾选完了测试不能瞎点“通过”。Playwright提供了非常好用的断言方法但我更推荐用expect静态方法它自带一个可配置的轮询等待比直接读属性更抗抖动。import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; assertThat(maleRadio).isChecked();如果你想在业务代码里判断状态那就用isChecked()方法if (maleRadio.isChecked()) { System.out.println(男生已选中); } else { System.out.println(仍未选中); }关于单选按钮还有一个特别容易踩的坑同一组里的单选按钮是互斥的。自动化勾选“女”时Playwright会自动处理互斥吗答案是不会。它只是老老实实地给当前元素发点击事件不会帮你取消同组其他按钮的选中。所以脚本里如果要切换选择必须自己先勾选新的那个再断言旧的那个已取消顺序反了就会造成“同时选中”的假象。Locator femaleRadio page.locator(input[namegender][valuefemale]); femaleRadio.check(); assertThat(femaleRadio).isChecked(); assertThat(maleRadio).not().isChecked(); // 关键确认原来的被取消这段逻辑非常推荐写进用例里既验证了当前选择又验证了组内互斥一举两得。4. 多选按钮操作详解4.1 批量勾选多个复选框的优雅做法多选按钮的外观和单选框长得不一样通常是方框HTML里的type为checkbox。场景最常见的是兴趣爱好看板比如下面这几个labelinput typecheckbox namehobby valuereading 阅读/label labelinput typecheckbox namehobby valuetravel 旅行/label labelinput typecheckbox namehobby valuemusic 音乐/label如果要把“阅读”和“旅行”都选上代码可以写成page.locator(input[namehobby][valuereading]).check(); page.locator(input[namehobby][valuetravel]).check();这里有个细节check()方法对复选框是“确保选中”但如果复选框本来就是选中的它不会重复点击也就不会出现“把选中的又取消”这种反直觉行为。这一点非常重要因为直接用click()就可能出这种问题自动化脚本在失败重跑时容易因为初装状态和数据残留而翻车。要是想批量遍历操作可以这样Locator hobbyBoxes page.locator(input[namehobby]); for (int i 0; i hobbyBoxes.count(); i) { hobbyBoxes.nth(i).check(); }批量勾选全部爱好在测试里挺常见遍历count()再nth(i).check()是一套很稳的写法。但是要注意这里的count()获取的是当前时刻节点的数量。如果页面是懒加载的可能需要先等待所有节点出现否则遍历过程中会漏选。4.2 断言多选按钮的三种状态多选按钮最大的坑是半选状态。比如一个“全选”复选框下挂了一堆子项如果只选了部分子项父级复选框的DOM会显示indeterminate属性但Playwright原生断言里没有isIndeterminate()这个方法需要我们自己用JavaScript去评估。我实操中最常用的写法Locator parentCheckbox page.locator(input#selectAll); boolean isIndeterminate (boolean) parentCheckbox.evaluate( el el.indeterminate );还有一种方式是直接评估DOM属性确认选中状态boolean isChecked (boolean) parentCheckbox.evaluate( el el.checked );对于普通子复选框常规断言用assertThat(childCheckbox).isChecked()就够了。我一般把三种状态的断言写成一个工具类状态判断方法备注选中isChecked()原生支持未选中not().isChecked()原生支持半选通过evaluate读indeterminate需要自己封装这样写测试的意义在于半选状态恰恰是业务逻辑中容易出错的环节。你点了5个子项中的2个父级一定要显示半选如果产品需求如此自动化就必须把这个状态锁定。5. 实操案例一个完整的测试流程5.1 设计一个包含单选和多选的表单页面纸上谈兵没什么意思我真正搭建了一个简单的订餐页来跑这套逻辑。页面里有三个字段选择餐品类型单选午餐、晚餐、选择配菜多选薯条、沙拉、饮料、是否加辣单选开关。整个页面由静态HTML和一点点原生JavaScript组成没有任何框架方便调试。关键DOM样式如下form idorderForm div classfield-group span餐品类型/span labelinput typeradio namemeal valuelunch 午餐/label labelinput typeradio namemeal valuedinner 晚餐/label /div div classfield-group span配菜选择/span labelinput typecheckbox nameside valuefries 薯条/label labelinput typecheckbox nameside valuesalad 沙拉/label labelinput typecheckbox nameside valuedrink 饮料/label /div button typesubmit提交/button /form测试目标很明确先选“午餐”再选“薯条”和“沙拉”提交后验证页面返回的摘要。这个案例虽然简单但能把单选、多选、提交、断言全部串起来。5.2 编写并跑通自动化脚本打开页面之后第一步是page.setContent()加载本地HTML也可以用本地文件协议。我这里用静态HTML塞进去最简单page.navigate(file:///path/to/order.html);然后依次操作page.locator(input[namemeal][valuelunch]).check(); page.locator(input[nameside][valuefries]).check(); page.locator(input[nameside][valuesalad]).check(); assertThat(page.locator(input[namemeal][valuelunch])).isChecked(); assertThat(page.locator(input[nameside][valuefries])).isChecked(); assertThat(page.locator(input[nameside][valuesalad])).isChecked();接着就是点击提交按钮并验证页面上出现的摘要信息。我用了一个比较校验的方式先读取提交后用locator.textContent()拿到文本再判断是否包含预期值。page.click(button[typesubmit]); String summary page.locator(#summary).textContent(); assert summary.contains(午餐) : 提交结果中没有午餐; assert summary.contains(薯条) summary.contains(沙拉) : 配菜选择不正确;跑完之后控制台看到绿色通过我的第一感受是这种代码比当年的Selenium干净太多了。尤其是不用再写线程睡眠等待也不需要到处封装ExpectedConditions核心业务逻辑一眼能看懂。6. 常见问题与排查技巧实录6.1 “元素定位不到”和“定位到多个元素”的区分这个错误几乎每个新手都会碰到。Playwright抛异常时如果提示“strict mode violation”意思就是你的定位符匹配到了多个元素需要进一步缩小范围。这时候别急着改代码先到Chrome DevTools里复制一下元素的完整CSS路径或者查看是否有ifame包裹。单选多选按钮经常被包在iframe里定位之前必须切换FrameLocator frame page.frameLocator(iframe#mainFrame); frame.locator(input[namemeal][valuelunch]).check();如果完全找不到可能是元素压根没加载这时就得检查是不是登录态或者权限问题。建议先在普通浏览器里手动操作一遍再回到自动化环境。6.2 点击了没反应勾选状态没改变这是多选按钮的重灾区尤其那些使用自定义UI的复选框。原生input被隐藏后check()可能会报“not visible”或直接静默失败。解决办法通常是临时改用JS强制点击但我不建议一上来就上JavaScript因为那会绕过Playwright的事件模拟和等待机制反而容易掩盖真实问题。更稳妥的顺序是检查原生input是否可见不可见则点击其对应的label或父级视觉元素实在不行才考虑使用click(new ClickOptions().setForce(true))强制点击最后靠isChecked()断言来确认结果。我踩得最久的一个坑是用了check()之后由于页面控件有动画状态还没刷新立刻断言就挂了。Playwright的expect断言自带等待所以只要用assertThat(...).isChecked()就不会有这个问题。如果你非要立刻用isChecked()读原始值那就要自己在前面加个page.waitForTimeout(300)但这种做法最好别进生产代码。6.3 半选状态断言不可靠的坑文档里说isChecked()只反映checked属性半选状态的indeterminate属性它不关心。所以前面我提到要用evaluate但还有一点容易被忽略indeterminate并不是HTML属性而是DOM属性用getAttribute(indeterminate)是拿不到东西的必须用el.indeterminate。这个细节坑了我整整一下午分享出来希望大家绕开。6.4 浏览器驱动安装失败的补救措施有次执行npx playwright install时因为网络问题一直失败直连下载几分钟就超时。后来我改用系统中已有的Chrome浏览器来跑只要在启动时指定channelbrowser playwright.chromium().launch(new BrowserType.LaunchOptions() .setChannel(chrome) .setHeadless(false));不过用系统Chrome有一个要注意的点它可能不支持Playwright所有的浏览器特性比如某些视频或权限API。但就操作单选多选按钮这个场景一点问题都没有。最后再分享一个小技巧。如果单选和多选按钮是页面渲染后才动态出现的最好在操作前用locator.waitFor()等待它变得可见而不是直接check()。我一开始偷懒不想写等待结果脚本在CI上偶发失败改了之后稳定性立马提高。另外建议所有交互代码都通过页面对象模型POM封装起来单选多选按钮本身虽然简单但业务层的变化会经常导致脚本调整封装好了之后每次改起来只需要动一个地方维护成本会低很多。这篇上篇先写到这下篇我准备聊一聊如何把单选多选的校验逻辑融合到数据驱动测试中去到时候见。