Playwright 定位器入门:七个 getBy 方法该先用哪个
写 UI 自动化最容易踩的坑,是把选择器绑在 DOM 结构上。前端改一次布局、加一层容器 div,#tsf > div:nth-child(2) > div.A8SBwf > ... 这种链条就断了,而页面在人眼里根本没变。Playwright 仓库的 docs/src/locators.md 对这类写法有明确态度:它把长 CSS / XPath 链称为 bad practice,并说这类选择器「绑在 DOM 结构或实现上,DOM 结构一变就会断」。
替代方案就是那一组 getBy* 方法。文档把它们统称为「recommended built-in locators」,并给出了一条总的取舍原则和每个方法各自的适用条件。这篇就把这条原则和七个方法的语义差别讲清楚。
一、先看仓库到底推荐了几个
docs/src/locators.md 的 Quick Guide 一节列的是「recommended built-in locators」,逐条数下来是七个:getByRole、getByText、getByLabel、getByPlaceholder、getByAltText、getByTitle、getByTestId。同一份文档在「Locating elements」开头写明的原则是:为了让测试更稳,推荐优先使用面向用户的属性和显式契约,并点名了 getByRole。
这句话就是取舍的来源——判据不是哪个方法写起来短,而是「这个信息对用户是不是可见的、是不是一份显式契约」。需要说明的是,除了 Quick Guide 的这个列举顺序和这句原则,文档并没有再给出一张从一到七的排名表,下面的顺序是我为了把表单相关的几个放在一起讲而调整过的。
还有一层机制值得先记住:文档写明,locator 每次被用于一个动作时,都会在页面上重新定位一次最新的 DOM 元素。所以下面这段里,底层元素会被定位两次,各在一个动作之前;如果两次调用之间页面重渲染了,用的就是重渲染之后对应的新元素。
const locator = page.getByRole('button', { name: 'Sign in' });
await locator.hover();
await locator.click();
理解了这一点,你就不会再去纠结「要不要把 locator 缓存起来复用」——它本来就是一份查找规则,不是一个元素句柄。
二、前置条件
这一段别跳过,几个细节容易在版本上绊住人。
- 可用版本:
docs/src/api/class-page.md里,七个getBy*方法都标着* since: v1.27。选项的 since 标记则落在共用模板文件docs/src/api/params.md里:getByRole的exact是since: v1.28,description是since: v1.60——也就是说方法本身早就有了,这两个选项是后来才补上的,更老的版本里没有它们。 - 不止 Page 上有。
docs/src/locators.md写明:所有创建 locator 的方法(比如getByLabel)在Locator和FrameLocator类上同样可用,所以可以链式调用逐步收窄。这一点是后面「链式定位」的基础。 - 语言绑定的命名不一样,别把 JS 的写法照搬到 Python 上。仓库同一段示例里,JS 是
page.getByRole('button', { name: 'Sign in' }),Python 是page.get_by_role("button", name="Sign in"),Java 是page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")),C# 是Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" })。Python 里的正则要用re.compile(...),Java 用Pattern.compile(...),C# 用new Regex(...)。 - 装环境。
docs/src/intro-js.md给的是npm init playwright@latest(该页按 npm / yarn / pnpm 分页签,没有按操作系统分页签,仓库里也没有给 Windows 单列另一条命令);Python 侧docs/src/intro-python.md给的是先pip install pytest-playwright,再执行playwright install。浏览器是 Playwright 自带并随版本绑定的,具体版本以官方发布说明为准,这里不展开。
三、七个方法逐个看
1. getByRole——默认的第一选择
文档对它的定位是:反映用户和辅助技术如何感知页面,比如某个元素到底是按钮还是复选框。用的时候通常还要传可访问名称,才能精确到某一个元素。仓库里的原始示例:
await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible();
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('button', { name: /submit/i }).click();
对应的 Python 写法在同一节里:
expect(page.get_by_role("heading", name="Sign up")).to_be_visible()
page.get_by_role("checkbox", name="Subscribe").check()
page.get_by_role("button", name=re.compile("submit", re.IGNORECASE)).click()
name 的匹配语义写在 docs/src/api/params.md 里:默认大小写不敏感、按子串匹配,要精确匹配得开 exact;而 exact 只在传字符串时生效,传正则时被忽略,并且精确匹配仍然会 trim 空白。这条很容易误判——你以为开了 exact 就是逐字节相等,其实两端空白还是被去掉的。
另外文档特意加了一句:role 定位器不能替代无障碍审计与合规测试,它只是就 ARIA 指南给出早期反馈。
2. getByLabel——表单字段用它
docs/src/api/params.md 里对它的描述比指南页更全:它能按关联 <label> 的文本定位,也能按 aria-labelledby 指向的元素,或者直接按 aria-label 属性。指南页给的最小示例是:
await page.getByLabel('Password').fill('secret');
文档给的适用场景就一句话:定位表单字段时用它。
3. getByPlaceholder——没有 label 才轮到它
适用条件文档写得很死:用于那些没有 label、但有 placeholder 文本的表单元素。原始示例:
await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
其中 name@example.com 和 playwright@microsoft.com 是仓库示例里的值,照抄它是为了让示例完整,不代表你的页面里也长这样。
4. getByText——留给非交互元素
它按元素包含的文本找,支持子串、精确字符串和正则三种匹配。
await expect(page.getByText('Welcome, John')).toBeVisible();
await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();
await expect(page.getByText(/welcome, [A-Za-z]+$/i)).toBeVisible();
两条容易忽略的说明:一是文本匹配总会规范化空白,即使开了精确匹配也一样——多个空格会并成一个,换行会变成空格,首尾空白被忽略;二是文档明确建议,文本定位器用来找 div、span、p 这类非交互元素,而 button、a、input 这类交互元素应该回到 role 定位器。
5. getByAltText——图片一类
适用于支持 alt 文本的元素,文档点名了 img 和 area:
await page.getByAltText('playwright logo').click();
6. getByTitle——元素带 title 属性时
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
这里的 Issues count 和 25 issues 同样是仓库示例 DOM 里的值。
7. getByTestId——最后的兜底,但也最稳
文档对它的评价有两面。一面是:按 test id 测试是最有韧性的方式,文本或 role 变了测试仍然通过;另一面是:test id 不是面向用户的,如果 role 或文本值本身对你很重要,就该回去用面向用户的定位器。
await page.getByTestId('directions').click();
默认读的属性是 data-testid——这是仓库当前文档里的默认值,随版本可能变动。要改的话,JS 侧在配置里:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});
其它绑定走 Selectors.setTestIdAttribute,方法签名在 docs/src/api/class-selectors.md 里。Python 侧仓库给的是 playwright.selectors.set_test_id_attribute("data-pw"),Java 是 playwright.selectors().setTestIdAttribute("data-pw"),C# 是 playwright.Selectors.SetTestIdAttribute("data-pw")。改完之后调用方式不变,仍然是 getByTestId('directions')。
四、边界:文档明说不支持和不推荐的地方
严格模式(strictness)会直接抛异常。 文档写明:所有隐含单个目标 DOM 元素的操作,只要匹配到多于一个元素就抛错。所以 page.getByRole('button').click() 在页面有多个按钮时是会失败的;而 page.getByRole('button').count() 这种明确的多元素操作则正常工作。想绕开严格检查可以用 first()、last()、nth(),但文档紧接着标了 not recommended:页面一变,Playwright 可能点到你没打算点的元素。
Shadow DOM 有两个例外。 所有定位器默认都能在 Shadow DOM 里工作,例外是:XPath 定位不穿透 shadow root,closed-mode 的 shadow root 不支持。
CSS / XPath 没被删掉,但被降级了。 page.locator() 仍然接受 css= / xpath= 前缀,省略前缀时会自动识别;文档的态度是「如果你绝对必须用」才用它,理由是 DOM 经常变、测试因此不稳。
元素太多时先 filter,别急着上 nth。 Locator.filter() 支持按文本(hasText / hasNotText)和按子孙元素(has / hasNot)筛,还能链式叠加。这里有个反直觉的点,文档专门用「✖ WRONG」标出来了:filter 里的定位器必须相对于外层定位器,它是从外层匹配到的元素开始查,而不是从文档根开始。文档举的反例是:外层是 page.getByRole('listitem'),filter 里却写了以 page.getByRole('list') 开头的定位器——<ul> 在 <li> 外面,从 <li> 匹配到的元素往里查永远查不到它,所以这条是匹配不上的。
两个组合操作符也在这一页里。 Locator.and() 用另一个 locator 去收窄已有的 locator,文档给的例子是把 role 和 title 两个条件叠在一起:page.getByRole('button').and(page.getByTitle('Subscribe'))(Python 侧因为 and 是关键字,方法名写作 and_)。Locator.or() 则匹配两个备选中的任意一个,适合「不知道会弹出按钮还是弹出对话框」的场景;文档同时提醒:如果两者同时出现在页面上,or 会同时匹配到,可能触发严格模式违规,这时候要配 first()。
只匹配可见元素用 filter({ visible: true }),但文档在这一节开头就提醒:通常更好的做法是找一个能唯一标识元素的方式,而不是靠可见性来筛。
Page-free locators 只在 JS 侧。 docs/src/locators.md 里那一节标着 * langs: js,讲的是用顶层 by 对象描述元素、再用 page.get() 绑定到页面。仓库写明 page.get(by.testId('list').text('Row')) 与 page.getByTestId('list').getByText('Row') 可以互换,且 By 是不可变的。其它语言绑定有没有对应能力,我们在仓库里没有找到对应说明。
五、怎么确认自己写对了
第一步用工具反推。docs/src/locators.md 的提示是:用 code generator 生成 locator,再按需要改。仓库 docs/src/codegen.md 给的启动方式是:
npx playwright codegen demo.playwright.dev/todomvc
同一页按语言分了页签:Python 侧是不带 npx 的 playwright codegen demo.playwright.dev/todomvc,Java 侧走的是 mvn exec:java 调 com.microsoft.playwright.CLI,C# 侧走 pwsh 调 playwright.ps1,别把 JS 那条直接搬过去。这一页还写了 Pick locator 的用法,不过那一节讲的是 VS Code 扩展里的流程:从 Testing 侧栏点 Pick locator,把鼠标移到浏览器窗口里的元素上,元素下方会高亮出对应的 locator,点中元素后它会出现在 VS Code 的 Pick locator 框里,按回车复制。
第二步用断言把「唯一性」钉住。文档在讲 filter 时用的就是这个办法——先筛出目标,再断言它只有一个:
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) }))
.toHaveCount(1);
第三步看报错。如果报错是 strict mode violation,按文档的说法就说明你的定位器匹配到了多个元素,这时候该做的是继续收窄(加 name、加 filter),而不是随手补一个 first()。
最后提一句:getBy* 这一层之外还有一批不常用的定位方式,仓库把它们单独放在 docs/src/other-locators.md 里,需要时再去翻。上面所有方法名、选项名和默认值都以仓库最新内容为准,该项目更新频繁。
本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测,
因此不涉及运行速度、稳定性与实际表现的任何描述。
该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。
本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。