关于Playwright定位问题:完整的定位心法
一、属性选择器揭秘
中括号[ ]叫属性选择器,是 CSS 标准里极其强大的定位方式。
写法:标签名[属性名="属性值"]
比如:
button[title="删除"] —— 找带有 title 属性且值完全等于"删除"的按钮。 input[placeholder*="名称"] —— *= 表示包含,只要 placeholder 里有"名称"二字就匹配。 div[class^="ant-"] —— ^= 表示以...开头,匹配 class 以 ant- 开头的 div。 span[data-testid$="-submit"] —— $= 表示以...结尾。中括号里还能写多个条件(且的关系):
button[title="删除"][disabled] /* title为删除,且带disabled属性 */ input[type="text"][name="username"] /* 多个属性同时匹配 */这个技巧专治那些没有role、没有文字、只有图标和图标的title的元素。
二、has:和filter——从一堆元素里筛出你的目标
Playwright 提供了两种“过滤”方式,作用都是在已匹配的集合里进一步缩小范围。
1.filter()—— 按条件过滤(属性、文本、子元素都可以)
// 1) 过滤出文本包含“保存”的按钮 page.locator('button').filter({ hasText: '保存' }) // 2) 过滤出内部包含某个子元素的父元素(has: 参数是另一个locator) page.locator('tr').filter({ has: page.locator('td', { hasText: '张三' }) }) // 3) 过滤出当前可见的、不带 disabled 属性的 page.locator('button').filter({ hasNot: page.locator('[disabled]') })关键理解:filter()是在你已经选中的一批元素上,按“包含什么文本 / 包含什么子元素 / 不包含什么子元素”再筛一遍。
2.has和hasText—— 直接在定位器内部过滤(推荐)
Playwright 的locator()支持直接传入has和hasText选项,和filter效果类似,但写法更紧凑:
// 直接找“内部包含td为张三”的tr page.locator('tr', { has: page.getByText('张三') }) // 找文本包含“删除”的按钮 page.locator('button', { hasText: '删除' }) // 找 class 为 card 的 div,且内部有一个 span 的文本是“详情” page.locator('div.card', { has: page.locator('span', { hasText: '详情' }) })区别记忆法:
locator('A', { has: locatorB }) :选那些内部含有 B 的 A。 locator('A').filter({ has: locatorB }) :和上面一样,但可以链式再继续过滤。 hasText: 'xxx' :是文本内容包含 xxx,不用再套一层 locator,常用于按钮、链接。三、父子兄弟关系的写法——拿到一个元素后,怎样找到它身边的人
Playwright 提供了locator的相对定位方法。
假设你已拿到某个元素:
const cell = page.getByRole('cell', { name: '张三' });找爸爸(父元素)
const row = cell.locator('..'); // CSS 的上级选择器 const row2 = cell.locator('xpath=..'); // 同样效果,用 XPath找爷爷(祖先)
const grandpa = cell.locator('../..'); // 上两级找兄弟(前面一个 / 后面一个)
const nextSibling = cell.locator('+ *'); // CSS 相邻兄弟选择器(紧挨着的下一个) const prevSibling = cell.locator('xpath=preceding-sibling::*[1]'); // XPath 前一个兄弟找儿子(后代元素)
cell.locator('button'); // cell 下的所有 button(子孙) cell.locator('> button'); // cell 的**直接儿子** button最常用的套路:先找到唯一行,再在行里找按钮。
const row = page.locator('tr', { has: page.getByText('张三') }); await row.locator('button[title="编辑"]').click();四、Playwright 定位武器库——除了 CSS,还有更多方法
| 方式 | 例子 | 适合场景 |
|---|---|---|
| CSS 选择器 | page.locator('.ant-btn-primary') | 通用,快 |
| XPath | page.locator('//button[text()="保存"]') | 文本精确匹配、路径复杂时 |
| 文本 | page.getByText('提交') | 直接按可见文字找 |
| 角色 | page.getByRole('button', { name: '保存' }) | 最优选择,符合无障碍 |
| 标签文本 | page.getByLabel('用户名') | 表单输入框 |
| 占位文本 | page.getByPlaceholder('请输入') | input 占位符 |
| 替代文本 | page.getByAltText('logo') | 图片 alt |
| 标题 | page.getByTitle('删除') | 等同于[title="删除"] |
| 测试 ID | page.getByTestId('submit-btn') | 自定义的稳定标记 |
推荐优先级:
Role > Label > Text > TestId > Attribute > CSS > XPath五、定位器的最佳实践规范(写出三年后还能绿的代码)
1. 不要依赖自动生成的 class
Ant Design 的css-1a2b3c4d、.ant-btn-primary这类随时会变,一升级全趴。
2. 不要用nth(1)做唯一性保障
页面多一个隐藏元素就会错位。如果你不得不用索引,请确保父容器已经唯一,比如row.locator('td').nth(2)是安全的,因为行已经唯一定位了。
3. 用data-testid给关键元素加“身份证”
在组件里给敏感按钮加上data-testid="create-module",测试里直接用page.getByTestId('create-module'),这是前端和测试最稳定的契约。
4. 凡是要等待的,都用waitFor()或断言可见性
await expect(submitBtn).toBeVisible({ timeout: 10000 }); await submitBtn.click();而不是直接click再 catch。
5. 封装“查找行”的方法
在页面对象里写一个findRow(username)方法,其他地方复用,避免每个方法都重复拼定位器。
六、实战串联:从一团乱麻到稳如磐石
需求:在表格中,点击“张三”那行的“重置密码”,并处理可能出现的确认弹窗。
最佳写法全流程:
// 1. 先确保表格加载完 await this.waitForTableLoaded(); // 2. 唯一定位行:通过两个 cell 的精确文本 const targetRow = this.page.locator('tr.ant-table-row', { has: this.page.getByRole('cell', { name: '张三', exact: true }) }).filter({ has: this.page.getByRole('cell', { name: 'zhangsan', exact: true }) }); // 3. 在行内找“重置密码”按钮(假设按钮用 title 区分) const resetBtn = targetRow.locator('button[title="重置密码"]'); await expect(resetBtn).toBeVisible(); // 4. 点击前把鼠标移走防止 tooltip await this.page.mouse.move(0, 0); await resetBtn.dispatchEvent('click'); // 5. 等待确认弹窗 const confirmDialog = this.page.getByRole('dialog'); await expect(confirmDialog).toBeVisible(); await confirmDialog.getByRole('button', { name: '确定' }).click();为什么稳?
行用两个 cell 互锁,绝无可能找到第二行。
按钮用
title属性,跟布局、顺序无关。移动鼠标 +
dispatchEvent从根源消灭 tooltip 遮挡。确认弹窗用
role='dialog',不依赖任何 class。
七、检验唯一性的工具箱
在写定位器的过程中,你可以随时在测试里验证:
// 方法1:打印匹配数量 console.log(await page.locator('button[title="删除"]').count()); // 方法2:断言唯一 await expect(page.locator('button[title="删除"]')).toHaveCount(1); // 方法3:用 page.pause() 打开 Inspector,在浏览器里实时调试 await page.pause(); // 方法4:用 .first() 暂时锁定第一个(临时方案,不推荐长期用) const btn = page.locator('button[title="删除"]').first();八、高阶组合技:一个locator走天下
// 1. 找“包含特定子元素的元素”的直接儿子 page.locator('ul.menu > li', { has: page.getByText('设置') }).locator('> a'); // 2. 多个 filter 链式串联 page.locator('div.card') .filter({ hasText: '环境配置' }) .filter({ has: page.locator('button[data-status="active"]') }) .locator('button', { hasText: '详情' }); // 3. 结合正则(文本可以使用正则) page.getByText(/创建.*模块/); page.locator('button', { hasText: /保存|确认/ });掌握了这些,你就不再是“找元素”,而是用规则计算元素。DOM 再怎么变,只要业务含义不变,你的定位就不会变。
把这些方法印在脑子里,它们就是你以后和 Flaky Tests 对线时最硬的底气。