Playwright Locators 完全指南:面向用户的内置定位器、过滤、链式操作与严格模式源码解析
本指南以 Playwright 仓库 docs/src/locators.md 为骨架,系统讲解 Playwright 的自动等待(auto-waiting)与可重试能力(retry-ability)的核心载体 —— Locator(定位器)。你将掌握七类内置定位器的适用场景与精确匹配、Shadow DOM 穿透、基于文本/子元素/可见性的过滤、and/or/first/nth 等算子,以及严格模式(strictness)的触发与规避,并深入其底层选择器引擎实现,写出稳定、可读、面向用户的端到端测试。
什么是 Locator:为什么它是 Playwright 定位体系的中枢
在 Playwright 中,Locator 代表“在任何时刻于页面中查找某个/某些元素的方式”,是整个自动等待与重试机制的中心。与一次性返回 DOM 句柄的旧式 API(如 $/$$)不同,Locator 是一个惰性、可重用的查询描述:每次用它执行动作时,Playwright 都会在那一刻重新到页面中解析一次,取回最新的 DOM 元素。
这一点在 packages/playwright-core/src/client/locator.ts 中得到印证:Locator 对象本质上只是保存了 _frame(所属 Frame)与一条 _selector 字符串,构造函数把各种过滤条件拼装成内部选择器(如 >> internal:has-text=...、>> internal:has=...、>> visible=true、>> nth=0),见 locator.ts#L53-L82。真正“取元素”的动作发生在每次调用时:动作方法(click、hover、fill 等)都会通过 _withElement 走一次 waitForSelector({ selector, strict: true, state: 'attached' })(见 locator.ts#L84-L99),把解析出的最新元素交给具体动作。
由此带来一个重要性质:同一个 Locator 可以复用于多次操作,而无需担心页面在两次调用之间重渲染导致元素失效。例如下面这段代码中,底层 DOM 元素会被解析两次——一次在 hover 前,一次在 click 前——若两次调用之间 DOM 因 re-render 而替换,Playwright 会操作新元素:
const locator = page.getByRole('button', { name: 'Sign in' });
await locator.hover();
await locator.click();
locator = page.get_by_role("button", name="Sign in")
locator.hover()
locator.click()
快速选择指南(Quick Guide)
官方推荐的七类内置定位器及其适用场景如下,本文后续章节会逐一展开:
| 定位器 | 定位依据 | 推荐使用场景 |
|---|---|---|
page.getByRole() / get_by_role() |
显式与隐式的可访问性角色(ARIA role) | 按钮、复选框、标题、链接等可交互/语义化元素(最推荐) |
page.getByText() / get_by_text() |
元素文本内容 | div、span、p 等非交互元素 |
page.getByLabel() / get_by_label() |
关联 <label> 的文本 |
表单控件 |
page.getByPlaceholder() / get_by_placeholder() |
placeholder 属性 |
无 label 但有占位符的输入框 |
page.getByAltText() / get_by_alt_text() |
alt 文本替代 |
img、area 等支持 alt 的元素 |
page.getByTitle() / get_by_title() |
title 属性 |
带 title 提示的元素 |
page.getByTestId() / get_by_test_id() |
data-testid 属性(可自定义) |
无法用上述方式唯一定位时,或采用 test id 方法论 |
一个贯穿典型登录流程的示例(用户名字段、密码字段、登录按钮、欢迎文案断言一气呵成):
await page.getByLabel('User Name').fill('John');
await page.getByLabel('Password').fill('secret-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome, John!')).toBeVisible();
page.get_by_label("User Name").fill("John")
page.get_by_label("Password").fill("secret-password")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Welcome, John!")).to_be_visible()
Java、C# 中同一套 API 全部可用:Java 使用
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")),C# 使用Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" })的语法风格。各方法在 Java(Page.GetByRoleOptions)与 C# 中均有对应选项类。本文为保持篇幅,代码示例统一用 JavaScript 与 Python 呈现;涉及的语言差异会在必要处注明。
查找元素(Locating elements)
Playwright 内置多类定位器。官方建议优先采用面向用户的属性与显式契约(如 getByRole),因为这类定位与用户及辅助技术(assistive technology)对页面的感知一致,测试对 DOM 结构调整的耐受性最强。
例如针对下面的 DOM:
<button>Sign in</button>
按其 button 角色 + 名字 "Sign in" 定位:
await page.getByRole('button', { name: 'Sign in' }).click();
page.get_by_role("button", name="Sign in").click()
提示:可以使用 code generator(代码生成器) 自动生成定位器,再按需手工微调。
还有一条实用技巧:所有创建定位器的 getBy* 方法不仅存在于 Page 上,也同样存在于 Locator 与 FrameLocator 类上,因此你可以链式调用、逐步缩小范围。比如先进入 iframe 再定位其中的按钮:
const locator = page
.frameLocator('#my-frame')
.getByRole('button', { name: 'Sign in' });
await locator.click();
locator = page.frame_locator("#my-frame").get_by_role("button", name="Sign in")
locator.click()
从源码看,locator.ts#L175-L213 的 locator()、getByRole()、getByText()、frameLocator() 等方法都在当前选择器之后追加 >> 拼接的后续片段,这从实现层面解释了“从 Page 逐步收窄到子元素”的链式机制:每段选择器串接成一个越来越具体的查询链,而 frameLocator 则在此基础上再包裹一层 frame 定位。
按角色定位(Locate by role)
getByRole 反映的是用户与辅助技术对页面的感知方式——比如某元素究竟是个按钮还是复选框。按角色定位时通常还应同时传入可访问名称(accessible name),让定位器精确命中唯一元素。
例如针对以下 DOM:
<h3>Sign up</h3>
<label>
<input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>
可按每个元素的内隐角色逐一命中:
await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible();
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('button', { name: /submit/i }).click();
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既支持精确字符串,也支持正则表达式(如上例/submit/i忽略大小写匹配 "Submit")。- 角色定位器涵盖按钮、复选框、标题、链接、列表、表格等大量角色,遵循 W3C 的 ARIA role / ARIA attributes / accessible name 规范;许多 HTML 元素(如
<button>)具有可被角色定位器识别出的隐式角色。
角色定位器的可用过滤属性与合法角色(源码视角)
在 packages/isomorphic/locatorUtils.ts#L19-L30 中定义了 ByRoleOptions 的全部选项:checked、description、disabled、exact、expanded、includeHidden、level、name、pressed、selected。每个选项最终被序列化成 internal:role=<role>[attr=value]... 形式的内部选择器(见 locatorUtils.ts#L69-L90)。
对应的匹配逻辑在 packages/injected/src/roleSelectorEngine.ts 中实现。这里有几个非常值得注意的“底层规则”:
- 某些属性只对特定角色合法。例如
checked只能用于 checkbox、radio 等角色,level只能用于 heading、listitem 等角色,expanded只适用于可展开角色;传错角色会被validateSupportedRole直接抛出带角色清单的错误(见 roleSelectorEngine.ts#L46-L49)。 name/description匹配前会做空白归一化(normalizeWhiteSpace),并且非exact模式下字符串name实际是大小写不敏感的子串匹配而非全等(见 roleSelectorEngine.ts#L165-L186)。- 角色判定依赖 packages/injected/src/roleUtils.ts 中的
getAriaRole、getElementAccessibleNameText、getAriaChecked等实现,这些读取同时受隐式 HTML 语义与显式aria-*属性影响。 - 查找会递归进入 open 的 shadow root(见 roleSelectorEngine.ts#L190-L200 的
query对element.shadowRoot的收集),这与下文“Shadow DOM 支持”一节互相印证。
:::note 何时用角色定位器 应优先使用角色定位器,因为它最接近用户与辅助技术感知页面的方式。 :::
同时需明确:角色定位器不能替代真正的可访问性审计(accessibility audits)与一致性测试,它只提供关于 ARIA 准则的早期反馈。
按标签定位(Locate by label)
多数表单控件带有专用 <label>,可用 getByLabel 依据关联标签的文本定位控件。例如下面的 DOM 把输入框包在 label 内:
<label>Password <input type="password" /></label>
await page.getByLabel('Password').fill('secret');
page.get_by_label("Password").fill("secret")
:::note 何时用标签定位器 在定位表单字段时应使用该定位器。 :::
其实现原理(见 locatorUtils.ts#L49-L51)是构造 internal:label= 内部选择器,并在注入侧按“label 元素文本 / for 属性关联 / aria-labelledby”等标准关联方式解析出真正的表单控件。
按占位符定位(Locate by placeholder)
输入框的 placeholder 属性用于提示用户应输入什么。可用 getByPlaceholder 定位:
<input type="email" placeholder="name@example.com" />
await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com")
:::note 何时用占位符定位器 适用于没有 label 但有 placeholder 文本的表单元素。 :::
从源码看,getByPlaceholder 与 alt/title 定位器共用同一条路径:getByAttributeTextSelector 生成 internal:attr=[placeholder=<text>](见 locatorUtils.ts#L32-L34 与 locatorUtils.ts#L61-L63),即它们本质上是按“某属性的文本”匹配的通用机制。
按文本定位(Locate by text)
用 getByText 依据元素包含的文本查找元素,支持子串、精确字符串或正则三种模式:
<span>Welcome, John</span>
await expect(page.getByText('Welcome, John')).toBeVisible();
expect(page.get_by_text("Welcome, John")).to_be_visible()
设置精确匹配(exact: true):
await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();
expect(page.get_by_text("Welcome, John", exact=True)).to_be_visible()
使用正则匹配:
await expect(page.getByText(/welcome, [A-Za-z]+$/i)).toBeVisible();
expect(page.get_by_text(re.compile("welcome, john", re.IGNORECASE))).to_be_visible()
:::note 文本匹配无论是否 exact 都会归一化空白:多个空格合并为一个、换行视为空格、并忽略首尾空白。 :::
:::note 何时用文本定位器
文本定位器适合找 div、span、p 等非交互元素;对 button、a、input 等交互元素请改用 角色定位器。
:::
当需要在列表中锁定某一项时,也可以组合使用 按文本过滤(filter by text)。
按 alt 文本定位(Locate by alt text)
图片都应携带描述自身的 alt 属性。getByAltText 依据该文本替代定位元素:
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
await page.getByAltText('playwright logo').click();
page.get_by_alt_text("playwright logo").click()
:::note 何时用 alt 定位器
当元素支持 alt 文本(如 img、area 元素)时使用。其在底层复用 internal:attr=[alt=...] 属性匹配。
:::
按 title 定位(Locate by title)
getByTitle 按元素的 title 属性定位:
<span title='Issues count'>25 issues</span>
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
expect(page.get_by_title("Issues count")).to_have_text("25 issues")
:::note 何时用 title 定位器
当元素具有 title 属性时使用(底层为 internal:attr=[title=...])。
:::
按 test id 定位(Locate by test id)
Test id 是最具弹性的定位方式:即使元素的文本或角色发生变化,测试依然通过。QA 与开发者应定义显式 test id 并使用 getByTestId 查询。但要注意,test id 并非用户可见;如果你关心角色或文本值本身,应优先使用 角色定位器 或 文本定位器。
<button data-testid="directions">Itinéraire</button>
await page.getByTestId('directions').click();
page.get_by_test_id("directions").click()
:::note 何时用 test id 定位器 当你选择采用 test id 方法论,或无法通过角色/文本定位时使用。 :::
配置自定义 test id 属性
默认 getByTestId 匹配 data-testid 属性,但你可以在测试配置中或通过 Selectors.setTestIdAttribute 改为其它属性名。
在测试配置(如 playwright.config.ts)中修改:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});
在测试代码中以编程方式修改:
playwright.selectors.set_test_id_attribute("data-pw")
此后 HTML 中即可改用 data-pw 作为 test id:
<button data-pw="directions">Itinéraire</button>
定位方式不变:
await page.getByTestId('directions').click();
page.get_by_test_id("directions").click()
从源码可看到该配置的具体走向:Locator.getByTestId 调用 getByTestIdSelector(testIdAttributeName(), testId)(见 locator.ts#L183-L185),生成 internal:testid=[data-testid=...] 形式的引擎选择器(见 locatorUtils.ts#L45-L47);属性名来自客户端 Selectors 中记录的可配置值,默认即 data-testid。
按 CSS 或 XPath 定位(万不得已时的选择)
如果你确实必须使用 CSS 或 XPath,可用 page.locator() 传入选择器。Playwright 同时支持 CSS 与 XPath,并且当你省略 css=/xpath= 前缀时会自动探测类型:
await page.locator('css=button').click();
await page.locator('xpath=//button').click();
await page.locator('button').click();
await page.locator('//button').click();
page.locator("css=button").click()
page.locator("xpath=//button").click()
page.locator("button").click()
page.locator("//button").click()
XPath 与 CSS 会紧密耦合 DOM 结构或实现细节,DOM 一旦调整极易失效。下面这类超长 CSS/XPath 链就是导致测试不稳定的坏实践典型:
await page.locator(
'#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input'
).click();
await page
.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input')
.click();
page.locator(
"#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input"
).click()
page.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input').click()
:::note 何时使用 CSS/XPath 不推荐 CSS 与 XPath,因为 DOM 经常变化会导致测试不具弹性。应尽量构造贴近用户感知的定位器(如 角色定位器),或用 test id 定义显式测试契约。 :::
从引擎实现看,CSS/XPath 之所以与 DOM 结构强耦合,是因为它们与角色/文本等“语义引擎”走完全不同的通道:XPath 由独立的 packages/injected/src/xpathSelectorEngine.ts 负责(也因此无法穿透 shadow root,见下一节),CSS 则在 packages/injected/src/selectorEngine.ts 定义的引擎框架中按选择器前缀路由到对应实现。
在 Shadow DOM 中定位
Playwright 的所有定位器默认即可作用于 Shadow DOM 中的元素,例外仅有两个:
- XPath 无法穿透 shadow root。
- closed 模式的 shadow root 不受支持。
考虑以下自定义 Web Component 结构:
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
可以像 shadow root 完全不存在一样定位。点击 shadow 内的 <div>Details</div>:
await page.getByText('Details').click();
page.get_by_text("Details").click()
用 hasText 选项把外部 <x-details> 与内部文本关联后点击它:
await page.locator('x-details', { hasText: 'Details' }).click();
page.locator("x-details", has_text="Details").click()
或用 toContainText 断言 <x-details> 包含文本 "Details":
await expect(page.locator('x-details')).toContainText('Details');
expect(page.locator("x-details")).to_contain_text("Details")
如前文所述,这种默认支持并非魔法:角色引擎的 query 在遍历过程中会收集每个元素的 shadowRoot 并递归查询(见 roleSelectorEngine.ts#L190-L200),因而语义类定位器天然具备 shadow DOM 穿透能力。
过滤定位器(Filtering Locators)
请看下面的 DOM:两个商品卡各有“Add to cart”按钮,我们想点击第二个商品卡的按钮。此时有多种过滤手段可选。
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
按文本过滤(Filter by text)
Locator.filter 按文本过滤。它会大小写不敏感地在元素内部(可能位于后代元素)搜索给定字符串,也支持正则:
await page
.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button', { name: 'Add to cart' })
.click();
page.get_by_role("listitem").filter(has_text="Product 2").get_by_role(
"button", name="Add to cart"
).click()
使用正则:
await page
.getByRole('listitem')
.filter({ hasText: /Product 2/ })
.getByRole('button', { name: 'Add to cart' })
.click();
page.get_by_role("listitem").filter(has_text=re.compile("Product 2")).get_by_role(
"button", name="Add to cart"
).click()
hasText/hasNotText 在构造 Locator 时被转写为 internal:has-text= 与 internal:has-not-text= 选择器片段(见 locator.ts#L57-L61)。
按“不具有”某文本过滤
也可反向过滤——不包含某文本的项:
// 5 件有货商品
await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(5);
# 5 件有货商品
expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)
按子元素/后代过滤(Filter by child/descendant)
filter 支持 has / hasNot 选项:只选中“包含/不包含某个匹配另一 Locator 的后代”的元素。该子定位器可以是任意其它定位器(getByRole、getByTestId、getByText 等):
await page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Add to cart' })
.click();
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
).get_by_role("button", name="Add to cart").click()
还可以断言过滤结果唯一,确保确实只有一个匹配卡片:
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) }))
.toHaveCount(1);
expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
)
).to_have_count(1)
关键约束:作为过滤条件的 Locator 必须“相对于”原 Locator,它从原 Locator 的匹配结果开始查询,而不是从文档根节点开始。因此下面的写法是错误的——getByRole('list') 会匹配到位于 <li> 之外的 <ul> 列表,而原 Locator getByRole('listitem') 匹配的是 <li>,两者范围不吻合:
// ✖ 错误写法
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('list').getByText('Product 2') }))
.toHaveCount(1);
# ✖ 错误写法
expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("list").get_by_role("heading", name="Product 2")
)
).to_have_count(1)
按“不具有”某子元素/后代过滤
同理支持 hasNot:过滤掉内部包含某匹配元素的项。
await expect(page
.getByRole('listitem')
.filter({ hasNot: page.getByText('Product 2') }))
.toHaveCount(1);
expect(
page.get_by_role("listitem").filter(
has_not=page.get_by_role("heading", name="Product 2")
)
).to_have_count(1)
同样地,hasNot 内部定位器也是从外层 Locator 的匹配起点开始匹配,而非文档根节点。实现上,has/hasNot 会要求内层 Locator 与外层同属一个 frame(见 locator.ts#L63-L75),否则会抛出 "Inner has locator must belong to the same frame" 之类的错误。
Locator 运算(Locator operators)
在一个 Locator 内部继续匹配(链式收窄)
你可以通过链式调用 getByText、getByRole 等持续把搜索范围收窄到页面特定部分。下面先定义 product 定位器,再在其内部定位按钮并点击、并断言唯一性:
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: 'Add to cart' }).click();
await expect(product).toHaveCount(1);
product = page.get_by_role("listitem").filter(has_text="Product 2")
product.get_by_role("button", name="Add to cart").click()
还可以把两个已存在的 Locator 拼接到一起,例如在某个对话框(dialog)内找 “Save” 按钮:
const saveButton = page.getByRole('button', { name: 'Save' });
// ...
const dialog = page.getByTestId('settings-dialog');
await dialog.locator(saveButton).click();
save_button = page.get_by_role("button", name="Save")
# ...
dialog = page.get_by_test_id("settings-dialog")
dialog.locator(save_button).click()
这条路径在源码中对应 Locator.locator(selectorOrLocator):传入字符串则直接拼接 >>,传入另一个 Locator 则封装为 internal:chain=<json> 片段,并要求二者属于同一 frame(见 locator.ts#L175-L181)。
同时匹配两个条件:and
Locator.and 通过“同时满足另一个 Locator”来收窄既有定位器。例如把按角色与按 title 结合:
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
button = page.get_by_role("button").and_(page.get_by_title("Subscribe"))
源码中它把第二个 Locator 序列化为 >> internal:and=<json> 片段(见 locator.ts#L255-L259),即最终元素必须同时通过两段查询。
匹配多个备选之一:or
当目标元素可能以两种形态出现、而你无法预先确定是哪一种时,用 Locator.or 构造“匹配其中任意一个(或两者)”的定位器。例如要点击 “New email” 按钮,但有时页面会先弹出安全设置对话框:
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
if (await dialog.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click();
new_email = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm security settings")
expect(new_email.or_(dialog).first).to_be_visible()
if (dialog.is_visible()):
page.get_by_role("button", name="Dismiss").click()
new_email.click()
:::note
如果 “New email” 按钮与安全对话框同时出现,or 定位器会同时匹配两者,此时可能抛出“严格模式违规(strict mode violation)”错误。这种情况下可用 Locator.first 只匹配其中一个。
:::
只匹配可见元素(visible)
:::note 与其检查可见性,通常更推荐找到一种更可靠的方式来唯一标识目标元素。 :::
考虑页面同时存在一隐一显两个按钮:
<button style='display: none'>Invisible</button>
<button>Visible</button>
-
下面这行会同时匹配两个按钮,进而触发严格模式违规错误:
await page.locator('button').click(); -
而用
visible限定后,只会命中可见的第二个按钮并点击它:await page.locator('button').visible().click();page.locator("button").visible.click()
如需反过来匹配不可见元素,则用 Locator.filter 并把 visible 选项设为 false。从源码可见,visible() 本质上就是 new Locator(frame, selector, { visible: true }),会被追加 >> visible=true 片段(见 locator.ts#L77-L78 与 locator.ts#L219-L221)。有关“可见”的精确定义,可参考 可操作性指南(actionability)。
列表操作(Lists)
以如下水果列表为例,演示对“一组同构元素”的常见操作:
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
统计列表项数量
用 count 断言保证列表恰有 3 项:
await expect(page.getByRole('listitem')).toHaveCount(3);
expect(page.get_by_role("listitem")).to_have_count(3)
断言列表中全部文本
用 toHaveText 断言整组文本内容:
await expect(page
.getByRole('listitem'))
.toHaveText(['apple', 'banana', 'orange']);
expect(page.get_by_role("listitem")).to_have_text(["apple", "banana", "orange"])
获取指定列表项
获取列表中的特定项有多种途径:
1. 按文本获取 —— 直接用 getByText 定位并点击:
await page.getByText('orange').click();
page.get_by_text("orange").click()
2. 按文本过滤 —— 先按 listitem 角色,再过滤文本:
await page
.getByRole('listitem')
.filter({ hasText: 'orange' })
.click();
page.get_by_role("listitem").filter(has_text="orange").click()
3. 按 test id 获取 —— 需要先在 HTML 中加入 test id:
<ul>
<li data-testid='apple'>apple</li>
<li data-testid='banana'>banana</li>
<li data-testid='orange'>orange</li>
</ul>
await page.getByTestId('orange').click();
page.get_by_test_id("orange").click()
4. 按序号取第 n 项 —— 当列表元素完全相同、只能靠顺序区分时,用 first/last/nth:
const banana = await page.getByRole('listitem').nth(1);
banana = page.get_by_role("listitem").nth(1)
不过要谨慎使用序号:页面经常变化,序号定位很容易指向与预期完全不同的元素。应尽量构造能通过严格模式判定的唯一定位器。
源码上,nth/first/last 分别产生 nth=1、nth=0、nth=-1(负号表示从末尾倒数)的定位片段(见 locator.ts#L243-L253)。
链式组合多个过滤器(Chaining filters)
当元素具有多种相似特征时,可以把多个 filter 串联起来逐层收窄。以下面含 John/Mary 与 Say hello/Say goodbye 的 4 行列表为例,要对“Mary + Say goodbye”那行截图:
<ul>
<li>
<div>John</div>
<div><button>Say hello</button></div>
</li>
<li>
<div>Mary</div>
<div><button>Say hello</button></div>
</li>
<li>
<div>John</div>
<div><button>Say goodbye</button></div>
</li>
<li>
<div>Mary</div>
<div><button>Say goodbye</button></div>
</li>
</ul>
const rowLocator = page.getByRole('listitem');
await rowLocator
.filter({ hasText: 'Mary' })
.filter({ has: page.getByRole('button', { name: 'Say goodbye' }) })
.screenshot({ path: 'screenshot.png' });
row_locator = page.get_by_role("listitem")
row_locator.filter(has_text="Mary").filter(
has=page.get_by_role("button", name="Say goodbye")
).screenshot(path="screenshot.png")
执行后项目根目录下会生成 screenshot.png 文件。
罕见场景
对列表每个元素做操作
用 all() 迭代全部匹配元素:
for (const row of await page.getByRole('listitem').all())
console.log(await row.textContent());
for row in page.get_by_role("listitem").all():
print(row.text_content())
或先 count() 再用传统 for 循环 + nth(i) 逐个访问:
const rows = page.getByRole('listitem');
const count = await rows.count();
for (let i = 0; i < count; ++i)
console.log(await rows.nth(i).textContent());
rows = page.get_by_role("listitem")
count = rows.count()
for i in range(count):
print(rows.nth(i).text_content())
在页面内求值(evaluate in the page)
Locator.evaluateAll 中的代码运行在页面上下文中,可以调用任意 DOM API:
const rows = page.getByRole('listitem');
const texts = await rows.evaluateAll(
list => list.map(element => element.textContent));
rows = page.get_by_role("listitem")
texts = rows.evaluate_all("list => list.map(element => element.textContent)")
严格模式(Strictness)
Locator 是严格的:所有隐含“必须命中唯一目标元素”的操作,一旦匹配到超过一个元素就会抛出异常。例如当 DOM 中存在多个按钮时,下面调用会直接报错:
await page.getByRole('button').click();
page.get_by_role("button").click()
但 Playwright 能识别出哪些是“面向多个元素”的操作,因此下面这类调用在匹配到多个元素时完全正常:
await page.getByRole('button').count();
page.get_by_role("button").count()
如果需要显式“退出”严格模式,可以在匹配到多个元素时通过 Locator.first、Locator.last、Locator.nth 指定取哪一个。但不推荐这么做——页面一旦变化,Playwright 很可能点到你并不想点的元素。正确做法是遵循上文的最佳实践,构造出能唯一标识目标的 Locator。
从底层看,“strict: true”是 client 侧动作统一传入 Frame 通道的默认值(例如 locator.ts#L117-L119 的 click 即 this._frame.click(this._selector, { strict: true, ...options })),而多元素感知的方法(如 count、all、evaluateAll、elementHandles)走的是 _queryCount/$$/$$eval 等无需唯一的通道——这正是“严格但按需放行”的设计来源。
更进一步的定位器
本文覆盖了最常用的七类内置定位器及过滤/运算/严格模式。对于其它相对少用的定位器(例如 getByRole 之外基于名称/几何的定位、internal: 特殊引擎等),可继续阅读仓库内配套指南 other-locators。
小结
- Locator 是自动等待与可重试的核心:它惰性描述“如何找元素”,每次动作都会重新解析最新 DOM,天然容忍重渲染。
- 优先面向用户:推荐次序是角色 → 文本/标签 → test id → CSS/XPath;文本与角色/标签定位器在底层走
internal:role/internal:text/internal:label等语义引擎(见 locatorUtils.ts),因而能穿透 open shadow root,而 XPath 则不行。 - 过滤与收窄:
filter({ hasText / hasNotText / has / hasNot / visible })、链式getBy*、and、or均为同一选择器拼接机制的语义化封装(见 locator.ts)。 - 严格模式保证动作不歧义;面向集合的操作(count/all/evaluateAll)则不受影响。
- 测试实践层面,可用仓库内 tests/page 下的
locator-query.spec.ts、page-strict.spec.ts等用例观察各类定位器与严格模式的实际断言方式,将其作为你编写稳定测试的参考范本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00