定位器 (Locators)
Locator 是 Playwright 自动等待和重试能力的核心部分。简而言之,定位器代表了一种在任何时刻在页面上查找元素的方法。
快速指南
这些是推荐的内置定位器。
- page.getByRole(role[, options]) 通过显式和隐式可访问性属性进行定位。
- page.getByText(text[, options]) 通过文本内容进行定位。
- page.getByLabel(text[, options]) 通过关联标签的文本定位表单控件。
- page.getByPlaceholder(text[, options]) 通过占位符定位输入。
- page.getByAltText(text[, options]) 通过替代文本定位元素,通常是图像。
- page.getByTitle(text[, options]) 通过标题定位元素。
- page.getByTestId(testId) 根据其
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();
每次将定位器用于某些操作时,都会在页面中定位最新的 DOM 元素。因此,在下面的代码段中,底层 DOM 元素将被定位两次,在每次操作之前。这意味着如果 DOM 在调用之间由于重新渲染而发生变化,则将使用与定位器对应的新元素。
const locator = page.getByText('Submit');
// ...
await locator.hover();
await locator.click();
严格性 (Strictness)
定位器是严格的。这意味着如果多个元素匹配给定的选择器,则暗示某些目标 DOM 元素的所有定位器操作都将抛出异常。例如,如果 DOM 中有多个按钮,则以下调用将抛出异常:
await page.getByRole('button').click();
另一方面,Playwright 了解您何时执行多元素操作,因此当定位器解析为多个元素时,以下调用完全正常。
await page.getByRole('button').count();
您可以通过 locator.first()、locator.last() 和 locator.nth(index) 告诉 Playwright 在多个元素匹配时使用哪个元素,从而显式选择退出严格性检查。不推荐使用这些方法,因为当您的页面更改时,Playwright 可能会点击您不想要的元素。相反,请遵循以下最佳实践来创建唯一标识目标元素的定位器。
定位元素
Playwright 附带多种内置方法来创建定位器。为了使测试具有弹性,我们建议优先考虑面向用户的属性和显式契约,并为它们提供专用方法,例如 page.getByText(text[, options])。通常使用 代码生成器 生成定位器,然后根据需要对其进行编辑很方便。
await page.getByText('Log in').click();
如果您绝对必须使用 CSS 或 XPath 定位器,可以使用 page.locator(selector[, options]) 创建一个定位器,该定位器采用描述如何在页面中查找元素的 选择器。
请注意,所有创建定位器的方法,例如 page.getByLabel(text[, options]),也可以在 Locator 和 FrameLocator 类上使用,因此您可以链接它们并迭代地缩小定位器范围。
const locator = page.frameLocator('#my-frame').getByText('Submit');
await locator.click();
基于可访问属性定位
page.getByRole(role[, options]) 定位器反映了用户和辅助技术如何感知页面,例如某些元素是按钮还是复选框。当按角色定位时,通常也应该传递可访问名称,以便定位器精确定位确切的元素。
await page.getByRole('button', { name: /submit/i }).click();
await page.getByRole('checkbox', { checked: true, name: "Check me" }).check();
角色定位器遵循 ARIA role、ARIA attributes 和 accessible name 的 W3C 规范。
请注意,角色定位器 不能替代 可访问性审核和一致性测试,而是提供有关 ARIA 准则的早期反馈。
通过标签文本定位
大多数表单控件通常都有专用的标签,可以方便地用于与表单交互。在这种情况下,您可以使用 page.getByLabel(text[, options]) 通过其关联的标签定位控件。
例如,考虑以下 DOM 结构。
<label for="password">Password:</label><input type="password" id="password">
您可以在通过标签文本定位输入后填写它:
await page.getByLabel('Password').fill('secret');
通过占位符文本定位
输入可能具有占位符属性,以向用户提示应输入什么值。您可以使用 page.getByPlaceholder(text[, options]) 定位此类输入。
例如,考虑以下 DOM 结构。
<input id="email" name="email" type="email" placeholder="name@example.com">
您可以在通过占位符文本定位输入后填写它:
await page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
通过文本定位
查找元素的最简单方法是查找它包含的文本。使用 page.getByText(text[, options]) 时,您可以按子字符串、精确字符串或正则表达式进行匹配。
await page.getByText('Log in').click();
await page.getByText('Log in', { exact: true }).click();
await page.getByText(/log in$/i).click();
您还可以在以其他方式定位时 按文本过滤,例如在列表中查找特定项目。
await page.getByTestId('product-item').filter({ hasText: 'Playwright Book' }).click();
按文本匹配总是规范化空白,即使是精确匹配。例如,它将多个空格转换为一个,将换行符转换为空格,并忽略前导和尾随空格。
通过替代文本 (Alt Text) 定位
所有图像都应具有描述图像的 alt 属性。您可以使用 page.getByAltText(text[, options]) 根据替代文本定位图像。
例如,考虑以下 DOM 结构。
<img alt="playwright logo" src="/playwright-logo.png" />
您可以在通过替代文本定位图像后点击它:
await page.getByAltText('playwright logo').click();
通过标题定位
使用 page.getByTitle(text[, options]) 定位具有匹配 title 属性的元素。
例如,考虑以下 DOM 结构。
<span title='Issues count'>25 issues</span>
您可以在通过标题文本定位后检查问题计数:
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
定义显式契约并使用 data-testid 属性
面向用户的属性(如文本或可访问名称)可能会随着时间的推移而改变。在这种情况下,定义显式测试 ID 并使用 page.getByTestId(testId) 查询它们很方便。
<button data-testid="directions">Itinéraire</button>
await page.getByTestId('directions').click();
默认情况下,page.getByTestId(testId) 将根据 data-testid 属性定位元素,但您可以在测试配置中配置它,或调用 selectors.setTestIdAttribute(attributeName)。
在子树中定位
您可以链接创建定位器的方法,例如 page.getByText(text[, options]) 或 locator.getByRole(role[, options]),以将搜索范围缩小到页面的特定部分。
例如,考虑以下 DOM 结构:
<div data-testid='product-card'>
<span>Product 1</span>
<button>Buy</button>
</div>
<div data-testid='product-card'>
<span>Product 2</span>
<button>Buy</button>
</div>
例如,我们可以首先找到包含文本 "Product 2" 的产品卡,然后点击此特定产品卡中的按钮。
const product = page.getByTestId('product-card').filter({ hasText: 'Product 2' });
await product.getByText('Buy').click();
通过 CSS 或 XPath 选择器定位
Playwright 支持 CSS 和 XPath 选择器,如果您省略 css= 或 xpath= 前缀,它会自动检测它们。使用 page.locator(selector[, options]) 来实现这一点:
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();
相反,尝试想出一个接近用户感知页面的定位器,或 定义显式测试契约。
定位包含其他元素的元素
按文本过滤
定位器可以选择按文本过滤。它将在元素内的某处(可能在后代元素中)不区分大小写地搜索特定字符串。您也可以传递正则表达式。
await page.getByTestId('product-card').filter({ hasText: 'Product 3' }).click();
await page.getByTestId('product-card').filter({ hasText: /product 3/ }).click();
按另一个定位器过滤
定位器支持仅选择具有匹配另一个定位器的后代的元素的选项。
page.getByRole('section').filter({ has: page.getByTestId('subscribe-button') })
请注意,内部定位器是从外部定位器开始匹配的,而不是从文档根目录开始。
增强现有定位器
您可以使用 locator.filter([options]) 方法按文本或另一个定位器过滤现有定位器,可能多次链接它。
const rowLocator = page.locator('tr');
// ...
await rowLocator
.filter({ hasText: 'text in column 1' })
.filter({ has: page.getByRole('button', { name: 'column 2 button' }) })
.screenshot();
在 Shadow DOM 中定位
Playwright 中的所有定位器 默认情况下 都适用于 Shadow DOM 中的元素。例外情况是:
- 通过 XPath 定位不会穿透 shadow roots。
- 不支持 Closed-mode shadow roots。
考虑以下带有自定义 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>await page.locator('x-details', { hasText: 'Details' }).click();确保
<x-details>包含文本 "Details"await expect(page.locator('x-details')).toContainText('Details');
列表
您还可以使用定位器来处理元素列表。
// 定位元素,此定位器指向一个列表。
const rows = page.getByRole('listitem');
// 模式 1:使用定位器方法计算整个列表上的文本。
const texts = await rows.allTextContents();
// 模式 2:对列表中的每个元素执行某些操作。
const count = await rows.count()
for (let i = 0; i < count; ++i)
console.log(await rows.nth(i).textContent());
// 模式 3:将定位器解析为页面上的元素并将它们映射到其文本内容。
// 注意:evaluateAll 内的代码在页面中运行,您可以在那里调用任何 DOM api。
const texts = await rows.evaluateAll(list => list.map(element => element.textContent));
从列表中选择特定元素
如果您有相同的元素列表,并且区分它们的唯一方法是顺序,则可以使用 locator.first()、locator.last() 或 locator.nth(index) 从列表中选择特定元素。
例如,要点击产品列表中的第三个项目:
await page.getByTestId('product-card').nth(3).click();
但是,请谨慎使用这些方法。很多时候,页面可能会发生变化,定位器将指向与您预期的完全不同的元素。相反,尝试想出一个通过 严格性标准 的唯一定位器。