定位器

介绍

定位器是 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 结构:

image 2024 12 17 18 26 24 612

可以通过按钮的角色和名称(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(),也可以在 LocatorFrameLocator 类中使用,因此您可以链式调用这些方法,并逐步缩小定位器的范围。

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

await locator.click();

通过角色(role)定位

page.getByRole() 定位器反映了用户和辅助技术如何感知页面,例如某个元素是否是按钮或复选框。在通过角色定位时,通常应该传递可访问名称,以确保定位器能够精确定位到正确的元素。

例如,考虑以下 DOM 结构。

image 2024 12 17 18 39 04 808

你可以通过元素的隐式角色来定位每个元素:

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

image 2024 12 17 18 42 03 646

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

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

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

通过占位符(placeholder)定位

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

例如,考虑以下 DOM 结构。

image 2024 12 17 18 43 34 156

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

await page
    .getByPlaceholder('name@example.com')
    .fill('playwright@microsoft.com');
何时使用 placeholder 定位器

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

通过文本(text)定位

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

例如,考虑以下 DOM 结构。

image 2024 12 17 18 45 02 981

您可以通过元素包含的文本来定位该元素:

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文本定位

所有图片都应该有一个 alt 属性来描述图片。您可以使用 page.getByAltText() 基于文本替代来定位图片。

例如,考虑以下 DOM 结构。

image 2024 12 17 18 48 23 269

您可以在通过文本选项找到图像后单击该图像:

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

当您的元素支持 alt 文本时,例如 imgarea 元素,使用此定位器。

通过标题(title)定位

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

例如,考虑以下 DOM 结构。

image 2024 12 17 18 50 00 392

您可以通过标题文本定位后检查问题数:

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

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

通过test id定位

通过测试 ID 进行测试是最具韧性的一种测试方式,因为即使你的文本或角色属性发生变化,测试仍然会通过。QA 和开发人员应定义明确的测试 ID,并使用 page.getByTestId() 进行查询。然而,通过测试 ID 进行测试并不是面向用户的。如果角色或文本值对你很重要,那么考虑使用面向用户的定位器,例如角色和文本定位器。

例如,考虑以下 DOM 结构。

image 2024 12 17 18 51 26 046

您可以通过它的测试 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

image 2024 12 17 18 54 02 605

然后像平常一样定位元素:

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 结构,我们想要点击第二个产品卡片的购买按钮。为了获取正确的元素,我们有几个过滤定位器的选项。

image 2024 12 17 18 59 47 289

通过文本过滤

定位器可以通过 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() 等。

image 2024 12 17 19 02 12 899
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);

通过not having child/descendant定位

我们也可以通过内部没有匹配元素来进行过滤。

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').locator('visible=true').click();

列表

统计列表中的项目

你可以通过断言定位器来统计列表中的项目数量。

例如,考虑以下 DOM 结构:

image 2024 12 17 19 17 49 867

使用 count 断言来确保列表中有 3 个项目。

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

断言列表中的所有文本

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

例如,考虑以下 DOM 结构:

image 2024 12 17 19 19 14 209

使用 expect(locator).toHaveText() 来确保列表中包含 "apple"、"banana" 和 "orange" 这几个文本。

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

获取特定项

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

通过文本获取

使用 page.getByText() 方法根据文本内容定位列表中的元素,并进行点击。

例如,考虑以下 DOM 结构:

image 2024 12 17 19 20 53 672

根据文本内容找到项目并单击它。

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

通过文本过滤

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

例如,考虑以下 DOM 结构:

image 2024 12 17 19 22 06 040

通过角色 "listitem" 定位项,然后通过文本 "orange" 过滤,最后点击该项。

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

Get by test id

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

例如,考虑以下 DOM 结构:

image 2024 12 17 19 23 25 229

通过测试 ID 为 "orange" 定位项目,然后点击它。

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

Get by nth item

如果你有一个相同元素的列表,且唯一能区分它们的方式是顺序,你可以使用 locator.first()locator.last()locator.nth() 来选择列表中的特定元素。

const banana = await page.getByRole('listitem').nth(1);

然而,使用这种方法时要小心。页面内容可能会发生变化,导致定位器指向与预期完全不同的元素。相反,尽量设计一个唯一的定位器,以满足严格的定位标准。

链式过滤器

当你有多个相似的元素时,可以使用 locator.filter() 方法来选择正确的元素。你还可以链式调用多个过滤器,以进一步缩小选择范围。

例如,考虑以下 DOM 结构:

image 2024 12 17 19 26 38 296

要截取包含 "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));

Strictness

定位器是严格的。这意味着,对定位器的所有操作,如果目标 DOM 元素匹配多个元素,会抛出异常。例如,以下调用如果 DOM 中有多个按钮,则会抛出错误:

如果匹配多个元素会抛出错误

await page.getByRole('button').click();

另一方面,Playwright 能够理解当你执行多个元素操作时,所以下列调用在定位器解析为多个元素时是完全正常的:

多个元素操作正常工作

await page.getByRole('button').count();

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

更多定位器

对于不常用的定位器,请参阅其他定位器指南。