首页
/ Playwright Locators 完全指南:面向用户的内置定位器、过滤、链式操作与严格模式源码解析

Playwright Locators 完全指南:面向用户的内置定位器、过滤、链式操作与严格模式源码解析

2026-09-06 19:07:41作者:庞队千Virginia

本指南以 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。真正“取元素”的动作发生在每次调用时:动作方法(clickhoverfill 等)都会通过 _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() 元素文本内容 divspanp 等非交互元素
page.getByLabel() / get_by_label() 关联 <label> 的文本 表单控件
page.getByPlaceholder() / get_by_placeholder() placeholder 属性 无 label 但有占位符的输入框
page.getByAltText() / get_by_alt_text() alt 文本替代 imgarea 等支持 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 上,也同样存在于 LocatorFrameLocator 类上,因此你可以链式调用、逐步缩小范围。比如先进入 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-L213locator()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 的全部选项:checkeddescriptiondisabledexactexpandedincludeHiddenlevelnamepressedselected。每个选项最终被序列化成 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 中的 getAriaRolegetElementAccessibleNameTextgetAriaChecked 等实现,这些读取同时受隐式 HTML 语义与显式 aria-* 属性影响。
  • 查找会递归进入 open 的 shadow root(见 roleSelectorEngine.ts#L190-L200queryelement.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-L34locatorUtils.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 何时用文本定位器 文本定位器适合找 divspanp 等非交互元素;对 buttonainput 等交互元素请改用 角色定位器。 :::

当需要在列表中锁定某一项时,也可以组合使用 按文本过滤(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 文本(如 imgarea 元素)时使用。其在底层复用 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 的后代”的元素。该子定位器可以是任意其它定位器(getByRolegetByTestIdgetByText 等):

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 内部继续匹配(链式收窄)

你可以通过链式调用 getByTextgetByRole 等持续把搜索范围收窄到页面特定部分。下面先定义 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-L78locator.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=1nth=0nth=-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.firstLocator.lastLocator.nth 指定取哪一个。但不推荐这么做——页面一旦变化,Playwright 很可能点到你并不想点的元素。正确做法是遵循上文的最佳实践,构造出能唯一标识目标的 Locator。

从底层看,“strict: true”是 client 侧动作统一传入 Frame 通道的默认值(例如 locator.ts#L117-L119clickthis._frame.click(this._selector, { strict: true, ...options })),而多元素感知的方法(如 countallevaluateAllelementHandles)走的是 _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*andor 均为同一选择器拼接机制的语义化封装(见 locator.ts)。
  • 严格模式保证动作不歧义;面向集合的操作(count/all/evaluateAll)则不受影响。
  • 测试实践层面,可用仓库内 tests/page 下的 locator-query.spec.tspage-strict.spec.ts 等用例观察各类定位器与严格模式的实际断言方式,将其作为你编写稳定测试的参考范本。
登录后查看全文
热门项目推荐
相关项目推荐