定位器
介绍
定位器是 Playwright 自动等待和重试能力的核心。简而言之,定位器代表了一种在页面上任何时刻查找元素的方法。
快速指南
以下是推荐的内置定位器:
-
page.getByRole():通过显式和隐式的可访问性属性来定位。
-
page.getByText():通过文本内容来定位。
-
page.getByLabel():通过与表单控件关联的标签文本来定位。
-
page.getByPlaceholder():通过占位符来定位输入框。
-
page.getByAltText():通过图片的替代文本来定位元素。
-
page.getByTitle():通过元素的 title 属性来定位。
-
page.getByTestId():通过元素的
data-testid
属性来定位(可以配置其他属性)。
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();
定位元素
Playwright 提供了多个内置定位器。为了使测试更具韧性,推荐优先使用用户可见的属性和显式约定,例如 page.getByRole()。
例如,考虑以下 DOM 结构:

可以通过按钮的角色和名称(Sign in)来定位:
await page.getByRole('button', { name: 'Sign in' }).click();
使用代码生成器生成一个定位器,然后根据需要进行编辑。 |
每次使用定位器执行操作时,都会在页面中定位到一个最新的 DOM 元素。在下面的代码片段中,底层的 DOM 元素会被定位两次,每次操作前都会重新定位。这意味着,如果在两次调用之间由于重新渲染而导致 DOM 发生变化,定位器将使用与之对应的新元素。
const locator = page.getByRole('button', { name: 'Sign in' });
await locator.hover();
await locator.click();
请注意,所有创建定位器的方法,如 page.getByLabel()
,也可以在 Locator
和 FrameLocator
类中使用,因此您可以链式调用这些方法,并逐步缩小定位器的范围。
const locator = page
.frameLocator('#my-frame')
.getByRole('button', { name: 'Sign in' });
await locator.click();
通过角色(role)定位
page.getByRole()
定位器反映了用户和辅助技术如何感知页面,例如某个元素是否是按钮或复选框。在通过角色定位时,通常应该传递可访问名称,以确保定位器能够精确定位到正确的元素。
例如,考虑以下 DOM 结构。

你可以通过元素的隐式角色来定位每个元素:
await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible();
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('button', { name: /submit/i }).click();
角色定位器包括按钮、复选框、标题、链接、列表、表格等,并遵循 W3C 对 ARIA 角色、ARIA 属性和可访问名称的规范。请注意,许多 HTML 元素,如 <button>
,都有一个隐式定义的角色,可以被角色定位器识别。
请注意,角色定位器并不能替代无障碍审计和合规性测试,它们更多的是提供关于 ARIA 指南的早期反馈。
何时使用角色定位器
我们建议优先使用角色定位器来定位元素,因为它是最接近用户和辅助技术感知页面的方式。 |
通过标签(label)定位
大多数表单控件通常都有专门的标签,可以方便地用来与表单进行交互。在这种情况下,你可以使用 page.getByLabel()
根据其关联的标签来定位控件。
例如,考虑以下 DOM 结构:

您可以在根据标签文本定位后填充输入:
await page.getByLabel('Password').fill('secret');
何时使用标签定位器
在定位表单字段时使用此定位器。 |
通过占位符(placeholder)定位
输入框可能具有 placeholder 属性,用于提示用户应该输入什么值。您可以使用 page.getByPlaceholder()
定位此类输入框。
例如,考虑以下 DOM 结构。

您可以在根据占位符文本定位后填充输入:
await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
何时使用 placeholder 定位器
当定位没有标签但有 placeholder 文本的表单元素时,使用此定位器。 |
通过文本(text)定位
通过元素包含的文本查找元素。使用 page.getByText()
时,可以通过子字符串、精确字符串或正则表达式进行匹配。
例如,考虑以下 DOM 结构。

您可以通过元素包含的文本来定位该元素:
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();
通过文本匹配时,总是会规范化空白字符,即使是精确匹配。例如,它会将多个空格合并为一个,将换行符转换为空格,并忽略前后的空白字符。 |
何时使用文本定位器
我们建议使用文本定位器来查找非交互元素,如 |
您还可以通过文本进行过滤,这在尝试查找列表中的特定项时非常有用。
通过alt文本定位
所有图片都应该有一个 alt
属性来描述图片。您可以使用 page.getByAltText()
基于文本替代来定位图片。
例如,考虑以下 DOM 结构。

您可以在通过文本选项找到图像后单击该图像:
await page.getByAltText('playwright logo').click();
何时使用 alt 定位器
当您的元素支持 alt 文本时,例如 |
通过标题(title)定位
使用 page.getByTitle()
根据匹配的 title 属性定位元素。
例如,考虑以下 DOM 结构。

您可以通过标题文本定位后检查问题数:
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
何时使用 title 定位器
当你的元素具有 title 属性时,使用此定位器。 |
通过test id定位
通过测试 ID 进行测试是最具韧性的一种测试方式,因为即使你的文本或角色属性发生变化,测试仍然会通过。QA 和开发人员应定义明确的测试 ID,并使用 page.getByTestId()
进行查询。然而,通过测试 ID 进行测试并不是面向用户的。如果角色或文本值对你很重要,那么考虑使用面向用户的定位器,例如角色和文本定位器。
例如,考虑以下 DOM 结构。

您可以通过它的测试 id 来定位元素:
await page.getByTestId('directions').click();
何时使用 testid 定位器
当你选择使用测试 ID 方法,或者无法通过角色或文本进行定位时,可以使用测试 ID。 |
设置自定义测试 ID 属性
默认情况下,page.getByTestId()
会根据 data-testid
属性定位元素,但你可以在测试配置中进行设置,或者通过调用 selectors.setTestIdAttribute()
来进行配置。
你可以设置测试 ID,使用自定义的数据属性进行测试。
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});
在你的 HTML 中,你现在可以使用 data-pw
作为测试 ID,而不是默认的 data-testid
。

然后像平常一样定位元素:
await page.getByTestId('directions').click();
通过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();
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();
何时使用此方法
不推荐使用 CSS 和 XPath,因为 DOM 结构通常会发生变化,从而导致测试不够稳定。相反,尝试使用更接近用户感知页面的定位器,如角色定位器,或者通过定义显式的测试合同来使用测试 ID。 |
在阴影DOM中定位
Playwright 中的所有定位器默认都适用于 Shadow DOM 元素。例外情况包括:
-
通过 XPath 定位不会穿透 shadow 根。
-
不支持闭合模式的 shadow 根。
考虑以下使用自定义 Web 组件的示例:
<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 一样进行定位。
要点击 <div>Details</div>
:
await page.getByText('Details').click();
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
要点击 <x-details>
:
await page.locator('x-details', { hasText: 'Details' }).click();
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
确保 <x-details>
包含文本 "Details":
await expect(page.locator('x-details')).toContainText('Details');
过滤定位器
考虑以下 DOM 结构,我们想要点击第二个产品卡片的购买按钮。为了获取正确的元素,我们有几个过滤定位器的选项。

通过文本过滤
定位器可以通过 locator.filter()
方法按文本进行过滤。它将在元素的内部某个位置(可能是子元素中)查找特定的字符串,且不区分大小写。你也可以传递一个正则表达式。
await page
.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button', { name: 'Add to cart' })
.click();
使用正则表达式:
await page
.getByRole('listitem')
.filter({ hasText: /Product 2/ })
.getByRole('button', { name: 'Add to cart' })
.click();
通过not having text定位
或者,通过不包含文本进行过滤:
// 5 in-stock items
await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(5);
通过child/descendant定位
定位器支持一个选项,可以仅选择具有或没有匹配另一个定位器的后代元素。因此,你可以通过任何其他定位器进行过滤,例如 locator.getByRole()
、locator.getByTestId()
、locator.getByText()
等。

await page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Add to cart' })
.click();
我们还可以断言产品卡片,确保只有一个:
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) }))
.toHaveCount(1);
过滤定位器必须相对于原始定位器,并且从原始定位器匹配的元素开始查询,而不是从文档根元素开始。因此,以下方法不起作用,因为过滤定位器从 <ul>
列表元素开始匹配,而这个元素位于原始定位器匹配的 <li>
列表项之外:
// ✖ WRONG
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('list').getByText('Product 2') }))
.toHaveCount(1);
定位操作符
在定位器内部匹配
您可以链式调用创建定位器的方法,例如 page.getByText()
或 locator.getByRole()
,以将搜索范围缩小到页面的特定部分。
在这个例子中,我们首先通过定位其角色为 listitem
创建一个名为 product
的定位器。然后我们通过文本进行过滤。接着,我们可以再次使用 product
定位器,通过角色定位按钮并点击它,然后使用断言确保页面上只有一个文本为 "Product 2" 的产品。
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: 'Add to cart' }).click();
await expect(product).toHaveCount(1);
您也可以将两个定位器链式组合在一起,例如,查找特定对话框内的 "Save" 按钮:
const saveButton = page.getByRole('button', { name: 'Save' });
// ...
const dialog = page.getByTestId('settings-dialog');
await dialog.locator(saveButton).click();
同时匹配两个定位器
locator.and()
方法通过匹配附加的定位器来缩小现有定位器的范围。例如,您可以结合使用 page.getByRole()
和 page.getByTitle()
来同时按角色和标题进行匹配。
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
匹配两个或更多替代定位器中的一个
如果您想要定位两个或更多元素中的一个,并且不确定是哪一个,可以使用 locator.or()
来创建一个匹配任意一个或两个替代项的定位器。
例如,考虑以下场景:您想要点击一个 "新邮件" 按钮,但有时安全设置对话框会弹出。在这种情况下,您可以等待 "新邮件" 按钮或对话框中的任意一个,并根据情况执行相应操作。
如果 "新邮件" 按钮和安全设置对话框都出现在屏幕上,"or" 定位器将会匹配到它们两个,这可能会导致 "严格模式违规" 错误。在这种情况下,您可以使用 |
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();
列表
统计列表中的项目
你可以通过断言定位器来统计列表中的项目数量。
例如,考虑以下 DOM 结构:

使用 count
断言来确保列表中有 3 个项目。
await expect(page.getByRole('listitem')).toHaveCount(3);
断言列表中的所有文本
可以通过断言定位器来查找列表中的所有文本。
例如,考虑以下 DOM 结构:

使用 expect(locator).toHaveText()
来确保列表中包含 "apple"、"banana" 和 "orange" 这几个文本。
await expect(page
.getByRole('listitem'))
.toHaveText(['apple', 'banana', 'orange']);
获取特定项
有多种方法可以获取列表中的特定项。
通过文本获取
使用 page.getByText()
方法根据文本内容定位列表中的元素,并进行点击。
例如,考虑以下 DOM 结构:

根据文本内容找到项目并单击它。
await page.getByText('orange').click();
通过文本过滤
使用 locator.filter()
方法在列表中定位特定项。
例如,考虑以下 DOM 结构:

通过角色 "listitem" 定位项,然后通过文本 "orange" 过滤,最后点击该项。
await page
.getByRole('listitem')
.filter({ hasText: 'orange' })
.click();
链式过滤器
当你有多个相似的元素时,可以使用 locator.filter()
方法来选择正确的元素。你还可以链式调用多个过滤器,以进一步缩小选择范围。
例如,考虑以下 DOM 结构:

要截取包含 "Mary" 和 "Say goodbye" 的行的屏幕截图:
const rowLocator = page.getByRole('listitem');
await rowLocator
.filter({ hasText: 'Mary' })
.filter({ has: page.getByRole('button', { name: 'Say goodbye' }) })
.screenshot({ path: 'screenshot.png' });
现在你应该在项目的根目录下有一个 "screenshot.png" 文件。
Strictness
定位器是严格的。这意味着,对定位器的所有操作,如果目标 DOM 元素匹配多个元素,会抛出异常。例如,以下调用如果 DOM 中有多个按钮,则会抛出错误:
如果匹配多个元素会抛出错误
await page.getByRole('button').click();
另一方面,Playwright 能够理解当你执行多个元素操作时,所以下列调用在定位器解析为多个元素时是完全正常的:
多个元素操作正常工作
await page.getByRole('button').count();
您可以通过告诉 Playwright 在多个元素匹配时使用哪个元素,显式地选择退出严格性检查,方法是通过 locator.first()、locator.last() 和 locator.nth()。这些方法不推荐使用,因为当页面发生变化时,Playwright 可能会点击一个您不打算点击的元素。相反,最好遵循上述最佳实践,创建一个能够唯一标识目标元素的定位器。