定位器
简介
[定位器]是 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
角色和名称 "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();
通过角色定位
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>密码 <input type="password" /></label>
您可以通过标签文本定位后填写输入框:
await page.getByLabel('密码').fill('secret');
在定位表单字段时使用此定位器。
通过占位符定位
输入框可能具有占位符属性,用于提示用户应输入的值。您可以使用 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
属性。您可以使用 page.getByAltText() 根据替代文本来定位图像。
例如,考虑以下 DOM 结构:
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
在通过替代文本定位后,可以点击该图片:
await page.getByAltText('playwright logo').click();
当您的元素支持替代文本(如 img
和 area
元素)时使用此定位器。
通过标题定位
使用 page.getByTitle() 方法通过匹配的 title 属性定位元素。
例如,考虑以下 DOM 结构:
<span title='Issues count'>25 issues</span>
在通过标题文本定位后,您可以检查问题数量:
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
当元素具有 title
属性时使用此定位器。
通过测试ID定位
使用测试ID进行测试是最健壮的方式,因为即使元素的文本或角色属性发生变化,测试仍能通过。质量保证人员和开发人员应定义明确的测试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=
前缀,会自动检测选择器类型。
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();
定位 Shadow DOM 中的元素
Playwright 中的所有定位器默认都可以与 Shadow DOM 中的元素一起工作。例外情况是:
- 通过 XPath 定位不会穿透 shadow root
- 不支持 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 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 结构,我们想要点击第二个产品卡的购买按钮。我们有几种选项可以筛选定位器以获取正确的那个。
Product 1
Product 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个有库存的项
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);
筛选定位器必须相对于原始定位器,并且从原始定位器匹配项开始查询,而不是从文档根节点开始。因此,以下代码不会生效,因为筛选定位器从 <ul>
列表元素开始匹配,而该元素位于原始定位器匹配的 <li>
列表项之外:
// ✖ 错误
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 定位器来获取按钮角色并点击它,然后使用断言来确保只有一个文本为 "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"定位器会同时匹配两者,可能会抛出"严格模式违规"错误。此时,您可以使用 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>
使用计数断言确保列表包含 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" 和 "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" 文件。
特殊用例
对列表中的每个元素执行操作
使用迭代器遍历元素:
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 可能会点击到你未预期的元素。相反,请遵循上述最佳实践来创建能唯一标识目标元素的定位器。
更多定位器
关于较少使用的定位器,请查看 其他定位器 指南。