定位符
介绍
定位符 是 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 结构。
<button>Sign in</button>
通过角色的 button
和名称“登录”来定位元素。
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();
按角色定位
page.getByRole() 定位符反映了用户和辅助技术如何感知页面,例如某个元素是按钮还是复选框。按角色定位时,通常也应传递可访问的名称,以便定位符精确定位到该元素。
例如,考虑以下 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();
角色定位符包括按钮、复选框、标题、链接、列表、表格等等,并遵循 W3C 关于 ARIA 角色、ARIA 属性和可访问名称的规范。请注意,许多 HTML 元素,如 <button>
,具有被角色定位符识别的隐式定义角色。
请注意,角色定位符不能替代可访问性审计和一致性测试,它们更多地是提供关于 ARIA 指南的早期反馈。
我们建议优先使用角色定位符来定位元素,因为这是最接近用户和辅助技术感知页面的方式。
按标签定位
大多数表单控件通常都有专用的标签,可以方便地用于与表单交互。在这种情况下,您可以使用 page.getByLabel() 根据其关联标签定位控件。
例如,考虑以下 DOM 结构。
<label>Password <input type="password" /></label>
定位后可以通过标签文本填充输入框
await page.getByLabel('Password').fill('secret');
在定位表单字段时使用此定位符。
按占位符定位
输入框可能包含 placeholder 属性,用于提示用户应输入的值。您可以使用 page.getByPlaceholder() 定位此类输入框。
例如,考虑以下 DOM 结构。
<input type="email" placeholder="name@example.com" />
定位后可以通过占位符文本填充输入框
await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
在定位没有标签但有占位符文本的表单元素时使用此定位符。
按文本定位
按元素包含的文本查找元素。使用 page.getByText() 时,您可以按子字符串、精确字符串或正则表达式进行匹配。
例如,考虑以下 DOM 结构。
<span>Welcome, John</span>
您可以通过元素包含的文本定位它
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
等,请使用角色定位符。
您还可以按文本过滤,这在尝试查找列表中的特定项目时非常有用。
按 Alt 文本定位
所有图像都应具有描述图像的 alt
属性。您可以使用 page.getByAltText() 根据文本替代内容定位图像。
例如,考虑以下 DOM 结构。
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
定位后可以通过文本替代内容点击图像
await page.getByAltText('playwright logo').click();
当您的元素支持 Alt 文本(如 img
和 area
元素)时使用此定位符。
按 Title 定位
使用 page.getByTitle() 定位具有匹配 title 属性的元素。
例如,考虑以下 DOM 结构。
<span title='Issues count'>25 issues</span>
定位后可以通过 Title 文本检查问题数量
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
当您的元素具有 title
属性时使用此定位符。
按测试 ID 定位
按测试 ID 进行测试是最具弹性的测试方式,因为即使您的文本或属性角色发生变化,测试仍然会通过。QA 和开发人员应定义明确的测试 ID,并使用 page.getByTestId() 进行查询。然而,按测试 ID 进行测试并非面向用户。如果角色或文本值对您很重要,则考虑使用面向用户的定位符,例如角色和文本定位符。
例如,考虑以下 DOM 结构。
<button data-testid="directions">Itinéraire</button>
您可以通过元素的测试 ID 定位它
await page.getByTestId('directions').click();
设置自定义测试 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
。
<button data-pw="directions">Itinéraire</button>
然后像通常一样定位元素
await page.getByTestId('directions').click();
按 CSS 或 XPath 定位
如果您绝对必须使用 CSS 或 XPath 定位符,可以使用 page.locator() 创建一个定位符,该定位符接受描述如何在页面中查找元素的选择器。Playwright 支持 CSS 和 XPath 选择器,如果您省略 css=
或 xpath=
前缀,Playwright 会自动检测它们。
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定义一个显式的测试约定。
在 Shadow DOM 中定位
Playwright 中的所有定位符默认情况下都适用于 Shadow DOM 中的元素。例外情况是:
- 按 XPath 定位不会穿透 Shadow Root。
- 不支持封闭模式的 Shadow Root。
考虑以下包含自定义 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 结构,我们想点击第二个产品卡片上的购买按钮。我们有几种方法可以过滤定位符以获取正确的定位符。
产品 1
产品 2
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
按文本过滤
定位符可以使用 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();
按不包含文本过滤
或者,按不包含文本过滤
// 5 in-stock items
await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(5);
按子元素/后代过滤
定位符支持一个选项,仅选择具有或不具有匹配另一个定位符的后代的元素。因此,您可以使用任何其他定位符进行过滤,例如 locator.getByRole()、locator.getByTestId()、locator.getByText() 等。
产品 1
产品 2
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
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);
过滤定位符必须相对于原始定位符,并从原始定位符匹配开始查询,而不是从文档根部开始。因此,以下代码将无法工作,因为过滤定位符从原始定位符匹配的 <li>
列表项之外的 <ul>
列表元素开始匹配
// ✖ WRONG
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('list').getByText('Product 2') }))
.toHaveCount(1);
按不具有子元素/后代过滤
我们还可以按不包含匹配元素进行过滤。
await expect(page
.getByRole('listitem')
.filter({ hasNot: page.getByText('Product 2') }))
.toHaveCount(1);
请注意,内部定位符是从外部定位符开始匹配的,而不是从文档根部开始。
定位符运算符
在定位符内部匹配
您可以链式调用创建定位符的方法,例如 page.getByText() 或 locator.getByRole(),以将搜索范围缩小到页面的特定部分。
在此示例中,我们首先通过定位其角色 listitem
创建一个名为 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);
您还可以将两个定位符链式组合在一起,例如查找特定对话框中的“保存”按钮
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”定位符将同时匹配它们,可能会抛出“严格模式违规”错误。在这种情况下,您可以使用 locator.first() 只匹配其中一个。
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();
仅匹配可见元素
通常最好找到更可靠的方法来唯一标识元素,而不是检查其可见性。
考虑一个页面上有两个按钮,第一个不可见,第二个可见。
<button style='display: none'>Invisible</button>
<button>Visible</button>
-
这将找到两个按钮并抛出严格性违规错误
await page.locator('button').click();
-
这将只找到第二个按钮,因为它可见,然后点击它。
await page.locator('button').filter({ visible: true }).click();
列表
计算列表中的项目数
您可以断言定位符以计算列表中的项目数。
例如,考虑以下 DOM 结构
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
使用 count 断言确保列表有 3 个项目。
await expect(page.getByRole('listitem')).toHaveCount(3);
断言列表中的所有文本
您可以断言定位符以查找列表中的所有文本。
例如,考虑以下 DOM 结构
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
使用 expect(locator).toHaveText() 确保列表包含文本“apple”、“banana”和“orange”。
await expect(page
.getByRole('listitem'))
.toHaveText(['apple', 'banana', 'orange']);
获取特定项目
有多种方法可以获取列表中的特定项目。
按文本获取
使用 page.getByText() 方法根据文本内容定位列表中的元素,然后点击它。
例如,考虑以下 DOM 结构
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
根据项目的文本内容定位并点击它。
await page.getByText('orange').click();
按文本过滤
使用 locator.filter() 定位列表中的特定项目。
例如,考虑以下 DOM 结构
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
根据角色的“listitem”定位项目,然后按文本“orange”过滤,然后点击它。
await page
.getByRole('listitem')
.filter({ hasText: 'orange' })
.click();
按测试 ID 获取
使用 page.getByTestId() 方法定位列表中的元素。如果还没有测试 ID,可能需要修改 HTML 并添加测试 ID。
例如,考虑以下 DOM 结构
- apple
- banana
- orange
<ul>
<li data-testid='apple'>apple</li>
<li data-testid='banana'>banana</li>
<li data-testid='orange'>orange</li>
</ul>
根据项目的测试 ID“orange”定位并点击它。
await page.getByTestId('orange').click();
按第 N 个项目获取
如果您有一个包含相同元素的列表,并且区分它们的唯一方法是顺序,则可以使用 locator.first()、locator.last() 或 locator.nth() 从列表中选择特定元素。
const banana = await page.getByRole('listitem').nth(1);
然而,谨慎使用此方法。通常情况下,页面可能会发生变化,定位符将指向与您预期完全不同的元素。相反,尝试提出一个能够通过严格性标准的唯一定位符。
链式过滤
当您有具有各种相似性的元素时,可以使用 locator.filter() 方法选择正确的元素。您还可以链式调用多个过滤器来缩小选择范围。
例如,考虑以下 DOM 结构
- John
- Mary
- John
- Mary
<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>
截取包含“Mary”和“说再见”的行的截图
const rowLocator = page.getByRole('listitem');
await rowLocator
.filter({ hasText: 'Mary' })
.filter({ has: page.getByRole('button', { name: 'Say goodbye' }) })
.screenshot({ path: 'screenshot.png' });
您的项目根目录中现在应该有一个“screenshot.png”文件。
罕见用例
对列表中的每个元素执行操作
迭代元素
for (const row of await page.getByRole('listitem').all())
console.log(await row.textContent());
使用常规 for 循环迭代
const rows = page.getByRole('listitem');
const count = await rows.count();
for (let i = 0; i < count; ++i)
console.log(await rows.nth(i).textContent());
在页面中评估
locator.evaluateAll() 内部的代码在页面中运行,您可以在那里调用任何 DOM API。
const rows = page.getByRole('listitem');
const texts = await rows.evaluateAll(
list => list.map(element => element.textContent));
严格性
定位符是严格的。这意味着对定位符执行的所有隐含目标 DOM 元素的操作,如果匹配多个元素,则会抛出异常。例如,如果在 DOM 中有多个按钮,以下调用将抛出异常
如果多于一个则抛出错误
await page.getByRole('button').click();
另一方面,Playwright 理解何时执行多元素操作,因此当定位符解析为多个元素时,以下调用可以正常工作。
适用于多个元素
await page.getByRole('button').count();
当多个元素匹配时,您可以通过 locator.first()、locator.last() 和 locator.nth() 明确选择退出严格性检查,告诉 Playwright 使用哪个元素。不推荐使用这些方法,因为当您的页面发生变化时,Playwright 可能会点击到您不打算点击的元素。相反,遵循上述最佳实践来创建唯一标识目标元素的定位符。
更多定位符
对于不太常用的定位符,请参阅其他定位符指南。