跳至主要内容

定位器

简介

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

http://localhost:3000
<button>Sign in</button>

通过其角色为button且名称为"Sign in"来定位该元素。

await page.getByRole('button', { name: 'Sign in' }).click();
note

使用代码生成器生成定位器,然后根据需要编辑它。

每次使用定位器执行操作时,都会在页面中定位最新的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结构。

http://localhost: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();

Role locators include buttons, checkboxes, headings, links, lists, tables, and many more and follow W3C specifications for ARIA role, ARIA attributes and accessible name. Note that many html elements like <button> have an implicitly defined role that is recognized by the role locator.

请注意,角色定位器并不能替代可访问性审计和合规性测试,而是提供关于ARIA指南的早期反馈。

When to use role locators

我们建议优先使用角色定位器来定位元素,因为这是最接近用户和辅助技术感知页面的方式。

通过标签定位

大多数表单控件通常都有专门的标签,可以方便地用于与表单交互。在这种情况下,您可以使用page.getByLabel()通过关联标签来定位控件。

例如,考虑以下DOM结构。

http://localhost:3000
<label>Password <input type="password" /></label>

您可以通过标签文本定位后填写输入框:

await page.getByLabel('Password').fill('secret');
When to use label locators

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

通过占位符定位

输入框可能有一个占位符属性,用于提示用户应输入什么值。您可以使用page.getByPlaceholder()来定位这样的输入框。

例如,考虑以下DOM结构。

http://localhost:3000
<input type="email" placeholder="name@example.com" />

您可以通过占位符文本定位后填写输入框:

await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
When to use placeholder locators

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

通过文本定位

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

例如,考虑以下DOM结构。

http://localhost:3000
Welcome, 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();
note

按文本匹配时总是会规范化空白字符,即使是精确匹配。例如,它会将多个空格转换为一个,将换行符转换为空格,并忽略开头和结尾的空白字符。

When to use text locators

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

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

通过alt文本定位

所有图片都应包含描述图片内容的alt属性。您可以使用page.getByAltText()通过替代文本来定位图片。

例如,考虑以下DOM结构。

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

您可以通过替代文本定位后点击图像:

await page.getByAltText('playwright logo').click();
When to use alt locators

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

通过标题定位

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

例如,考虑以下DOM结构。

http://localhost:3000
25 issues
<span title='Issues count'>25 issues</span>

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

await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
When to use title locators

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

通过测试ID定位

通过测试ID进行测试是最稳健的测试方式,因为即使属性文本或角色发生变化,测试仍能通过。质量保证人员和开发人员应定义明确的测试ID,并使用page.getByTestId()来查询它们。然而,通过测试ID进行的测试并不面向最终用户。如果角色或文本值对您很重要,那么请考虑使用面向用户的定位器,如roletext locators

例如,考虑以下DOM结构。

http://localhost:3000
<button data-testid="directions">Itinéraire</button>

您可以通过元素的测试ID来定位它:

await page.getByTestId('directions').click();
When to use testid locators

当您选择使用测试ID方法或无法通过roletext定位时,也可以使用测试ID。

设置自定义测试ID属性

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

设置测试ID以便在测试中使用自定义数据属性。

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

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

In your html you can now use data-pw as your test id instead of the default data-testid.

http://localhost: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();
When to use this

不建议使用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 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结构,我们想要点击第二个产品卡的购买按钮。为了筛选定位器以获取正确的那个,我们有几种选择。

http://localhost: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);

按子节点/后代节点筛选

定位器(Locators)支持一个选项,可以仅选择具有或不具有匹配另一个定位器的后代元素的元素。因此,您可以通过任何其他定位器进行过滤,例如locator.getByRole()locator.getByTestId()locator.getByText()等。

http://localhost: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);

过滤定位器必须是相对于原始定位器的,并且查询是从原始定位器匹配项开始的,而不是从文档根目录开始的。因此,以下方法将不起作用,因为过滤定位器是从<ul>列表元素开始匹配的,而该元素位于原始定位器匹配的<li>列表项之外:

// ✖ 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定位器来获取按钮角色并点击它,然后使用断言来确保只有一个文本为"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()创建一个匹配任意一个或两个备选项的定位器。

例如,考虑这样一个场景:您想要点击"新建邮件"按钮,但有时会弹出一个安全设置对话框。在这种情况下,您可以等待"新建邮件"按钮或对话框出现,然后采取相应的操作。

note

如果屏幕上同时出现"新邮件"按钮和安全对话框,"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();

仅匹配可见元素

note

通常来说,寻找一种更可靠的方式来唯一标识元素比检查可见性更好。

考虑一个页面有两个按钮,第一个不可见而第二个可见

<button style='display: none'>Invisible</button>
<button>Visible</button>
  • 这将找到两个按钮并抛出一个严格性违规错误:

    await page.locator('button').click();
  • 这只会找到第二个按钮,因为它是可见的,然后点击它。

    await page.locator('button').filter({ visible: true }).click();

列表

计算列表中的项目数量

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

例如,考虑以下DOM结构:

http://localhost:3000
  • apple
  • banana
  • 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

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

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

断言列表中的所有文本

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

例如,考虑以下DOM结构:

http://localhost:3000
  • apple
  • banana
  • 橙子
<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结构:

http://localhost:3000
  • apple
  • banana
  • 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

通过文本内容定位一个项目并点击它。

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

按文本筛选

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

例如,考虑以下DOM结构:

http://localhost:3000
  • apple
  • banana
  • 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

通过角色"listitem"定位一个项目,然后通过文本"orange"进行筛选,最后点击它。

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

通过测试ID获取

Use the page.getByTestId() method to locate an element in a list. You may need to modify the html and add a test id if you don't already have a test id.

例如,考虑以下DOM结构:

http://localhost:3000
  • apple
  • banana
  • 橙子
<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结构:

http://localhost:3000
  • 约翰
  • 玛丽
  • 约翰
  • 玛丽
<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));

严格模式

定位器(Locators)是严格的。这意味着如果匹配到多个DOM元素,所有基于定位器的操作(暗示需要特定目标DOM元素)都会抛出异常。例如,当DOM中存在多个按钮时,以下调用会抛出异常:

如果超过一个元素则抛出错误

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

另一方面,Playwright能够识别您执行的多元素操作,因此当定位器解析为多个元素时,以下调用可以完美运行。

与多个元素配合良好

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

您可以通过使用locator.first()locator.last()locator.nth()明确指定在多个元素匹配时使用哪个元素,从而选择退出严格性检查。不过不建议使用这些方法,因为当页面发生变化时,Playwright可能会点击到您不希望的元素。相反,请遵循上述最佳实践来创建一个能唯一标识目标元素的定位器。

更多定位器

对于较少使用的定位器,请查看其他定位器指南。