跳至主要内容

定位器

简介

定位器 是 Playwright 自动等待和重试功能的核心部分。简而言之,定位器代表了一种在任何时刻查找页面上元素(或元素组)的方法。

快速指南

以下是被推荐的内置定位器。

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 结构。

https://127.0.0.1:3000
<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())在 LocatorFrameLocator 类中也可用,因此您可以将它们链接起来并迭代地缩小定位器的范围。

const locator = page
.frameLocator('#my-frame')
.getByRole('button', { name: 'Sign in' });

await locator.click();

按角色定位

page.getByRole() 定位器反映了用户和辅助技术如何感知页面,例如某个元素是按钮还是复选框。按角色定位时,通常也应传递可访问名称,以便定位器精确定位元素。

例如,考虑以下 DOM 结构。

https://127.0.0.1:3000

注册


<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 结构。

https://127.0.0.1:3000
<label>Password <input type="password" /></label>

您可以根据标签文本定位输入框后填充它

await page.getByLabel('Password').fill('secret');
何时使用标签定位器

定位表单字段时使用此定位器。

按占位符定位

输入框可能具有占位符属性,以提示用户应输入什么值。您可以使用 page.getByPlaceholder() 定位此类输入框。

例如,考虑以下 DOM 结构。

https://127.0.0.1:3000
<input type="email" placeholder="[email protected]" />

您可以根据占位符文本定位输入框后填充它

await page
.getByPlaceholder('[email protected]')
.fill('[email protected]');
何时使用占位符定位器

定位没有标签但有占位符文本的表单元素时使用此定位器。

按文本定位

根据元素包含的文本查找元素。使用 page.getByText() 时,您可以匹配子字符串、精确字符串或正则表达式。

例如,考虑以下 DOM 结构。

https://127.0.0.1:3000
欢迎,John
<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();
注意

按文本匹配始终会规范化空格,即使使用精确匹配也是如此。例如,它会将多个空格转换为一个空格,将换行符转换为空格,并忽略前导和尾随空格。

何时使用文本定位器

我们建议使用文本定位器查找非交互式元素,如 divspanp 等。对于交互式元素,如 buttonainput 等,请使用 角色定位器

您还可以 按文本过滤,这在尝试查找列表中的特定项目时非常有用。

按备用文本定位

所有图像都应具有描述图像的 alt 属性。您可以使用 page.getByAltText() 根据备用文本定位图像。

例如,考虑以下 DOM 结构。

https://127.0.0.1:3000
playwright logo
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />

根据备用文本定位图像后,您可以单击它

await page.getByAltText('playwright logo').click();
何时使用备用文本定位器

当您的元素支持备用文本(如 imgarea 元素)时使用此定位器。

按标题定位

使用 page.getByTitle() 定位具有匹配 title 属性的元素。

例如,考虑以下 DOM 结构。

https://127.0.0.1:3000
25 个问题
<span title='Issues count'>25 issues</span>

根据标题文本定位后,您可以检查问题数量

await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
何时使用 title 定位器

当您的元素具有title属性时,使用此定位器。

通过测试 ID 定位

通过测试 ID 进行测试是测试最可靠的方法,即使您的文本或属性角色发生更改,测试仍然会通过。QA 和开发人员应该定义明确的测试 ID 并使用page.getByTestId()查询它们。但是,通过测试 ID 进行测试不面向用户。如果角色或文本值对您很重要,请考虑使用面向用户的定位器,例如角色文本定位器

例如,考虑以下 DOM 结构。

https://127.0.0.1:3000
<button data-testid="directions">Itinéraire</button>

您可以通过其测试 ID 定位元素

await page.getByTestId('directions').click();
何时使用 testid 定位器

当您选择使用测试 ID 方法或无法通过角色文本进行定位时,您也可以使用测试 ID。

设置自定义测试 ID 属性

默认情况下,page.getByTestId()将根据data-testid属性定位元素,但您可以在测试配置中或通过调用selectors.setTestIdAttribute()来配置它。

将测试 ID 设置为使用自定义数据属性进行测试。

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});

在您的 html 中,您现在可以使用data-pw作为您的测试 ID,而不是默认的data-testid

https://127.0.0.1:3000
<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();
何时使用此方法

不建议使用 CSS 和 XPath,因为 DOM 经常会发生变化,从而导致测试不稳定。相反,尝试想出一个更接近用户感知页面方式的定位器,例如角色定位器或使用测试 ID定义明确的测试契约

在 Shadow DOM 中定位

Playwright 中的所有定位器默认情况下都适用于 Shadow DOM 中的元素。例外情况是

考虑以下使用自定义 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 根不存在一样进行定位。

要单击<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 结构,我们希望单击第二个产品卡片上的购买按钮。为了过滤定位器以获得正确的定位器,我们有一些选择。

https://127.0.0.1:3000
  • 产品 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()等。

https://127.0.0.1:3000
  • 产品 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()创建一个匹配所有备选方案的定位器。

例如,考虑这样一种情况:您想单击“新电子邮件”按钮,但有时会显示安全设置对话框。在这种情况下,您可以等待“新电子邮件”按钮或对话框,然后采取相应的措施。

注意

如果“新电子邮件”按钮和安全对话框都显示在屏幕上,“或”定位器将匹配两者,可能会抛出“严格模式违规”错误。在这种情况下,您可以使用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').locator('visible=true').click();

列表

计算列表中的项目数

您可以断言定位器以计算列表中的项目数。

例如,考虑以下 DOM 结构

https://127.0.0.1:3000
  • 苹果
  • 香蕉
  • 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

使用计数断言确保列表中有 3 个项目。

await expect(page.getByRole('listitem')).toHaveCount(3);

断言列表中的所有文本

您可以断言定位器以查找列表中的所有文本。

例如,考虑以下 DOM 结构

https://127.0.0.1:3000
  • 苹果
  • 香蕉
  • 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

使用expect(locator).toHaveText()确保列表包含文本“苹果”、“香蕉”和“橙子”。

await expect(page
.getByRole('listitem'))
.toHaveText(['apple', 'banana', 'orange']);

获取特定项目

有很多方法可以获取列表中的特定项目。

按文本获取

使用page.getByText()方法按其文本内容在列表中定位元素,然后单击它。

例如,考虑以下 DOM 结构

https://127.0.0.1:3000
  • 苹果
  • 香蕉
  • 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

定位项目并根据其文本内容单击它。

await page.getByText('orange').click();

按文本过滤

使用locator.filter()在列表中定位特定项目。

例如,考虑以下 DOM 结构

https://127.0.0.1:3000
  • 苹果
  • 香蕉
  • 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

通过“listitem”的角色定位一个项目,然后按“橙子”的文本进行过滤,然后单击它。

await page
.getByRole('listitem')
.filter({ hasText: 'orange' })
.click();

通过测试 ID 获取

使用 page.getByTestId() 方法在列表中定位元素。如果您还没有测试 ID,则可能需要修改 html 并添加一个测试 ID。

例如,考虑以下 DOM 结构

https://127.0.0.1:3000
  • 苹果
  • 香蕉
  • 橙子
<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 结构

https://127.0.0.1:3000
  • 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();

您可以通过告诉 Playwright 当多个元素匹配时使用哪个元素来明确选择退出严格性检查,方法是使用 locator.first()locator.last()locator.nth()。这些方法**不推荐**,因为当您的页面发生更改时,Playwright 可能会点击您未打算点击的元素。相反,请遵循上述最佳实践来创建唯一标识目标元素的定位器。

更多定位器

对于不太常用的定位器,请查看 其他定位器 指南。