跳到主要内容

定位符

介绍

定位符 是 Playwright 自动等待和重试能力的核心。简而言之,定位符表示在任何时刻找到页面上一个或多个元素的方式。

快速指南

以下是推荐的内置定位符。

await Page.GetByLabel("User Name").FillAsync("John");

await Page.GetByLabel("Password").FillAsync("secret-password");

await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();

await Expect(Page.GetByText("Welcome, John!")).ToBeVisibleAsync();

定位元素

Playwright 内置了多种定位符。为了使测试具有弹性,我们建议优先使用面向用户的属性和显式契约,例如 Page.GetByRole()

例如,考虑以下 DOM 结构。

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

根据其 role 为 button 且名称为 "Sign in" 来定位元素。

await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
注意

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

每次对定位符执行操作时,都会在页面中定位最新的 DOM 元素。在下面的代码片段中,底层 DOM 元素将被定位两次,每次操作前一次。这意味着如果在调用之间由于重新渲染导致 DOM 发生变化,将使用与定位符对应的新元素。

var locator = Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" });

await locator.HoverAsync();
await locator.ClickAsync();

请注意,所有创建定位符的方法,例如 Page.GetByLabel(),也可用于 LocatorFrameLocator 类,因此您可以将它们链式调用并逐步缩小定位范围。

var locator = Page
.FrameLocator("#my-frame")
.GetByRole(AriaRole.Button, new() { Name = "Sign in" });

await locator.ClickAsync();

按角色定位

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

例如,考虑以下 DOM 结构。

http://localhost:3000

注册

<h3>Sign up</h3>
<label>
<input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>

您可以根据每个元素的隐式角色进行定位

await Expect(Page
.GetByRole(AriaRole.Heading, new() { Name = "Sign up" }))
.ToBeVisibleAsync();

await Page
.GetByRole(AriaRole.Checkbox, new() { Name = "Subscribe" })
.CheckAsync();

await Page
.GetByRole(AriaRole.Button, new() {
NameRegex = new Regex("submit", RegexOptions.IgnoreCase)
})
.ClickAsync();

角色定位符包括按钮、复选框、标题、链接、列表、表格等等,并遵循 W3C 关于ARIA 角色ARIA 属性可访问名称的规范。请注意,许多 html 元素(如 <button>)具有角色定位符可以识别的隐式定义角色

请注意,角色定位符不能替代辅助功能审计和一致性测试,它们更多是关于 ARIA 指南的早期反馈。

何时使用角色定位符

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

按标签定位

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

例如,考虑以下 DOM 结构。

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

定位到标签文本后,您可以填充输入框

await Page.GetByLabel("Password").FillAsync("secret");
何时使用标签定位符

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

按占位符定位

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

例如,考虑以下 DOM 结构。

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

定位到占位符文本后,您可以填充输入框

await Page
.GetByPlaceholder("name@example.com")
.FillAsync("playwright@microsoft.com");
何时使用占位符定位符

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

按文本定位

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

例如,考虑以下 DOM 结构。

http://localhost:3000
欢迎,John
<span>Welcome, John</span>

您可以根据元素包含的文本进行定位

await Expect(Page.GetByText("Welcome, John")).ToBeVisibleAsync();

设置精确匹配

await Expect(Page
.GetByText("Welcome, John", new() { Exact = true }))
.ToBeVisibleAsync();

使用正则表达式匹配

await Expect(Page
.GetByText(new Regex("welcome, john", RegexOptions.IgnoreCase)))
.ToBeVisibleAsync();
注意

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

何时使用文本定位符

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

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

按 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").ClickAsync();
何时使用 Alt 定位符

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

按 Title 定位

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

例如,考虑以下 DOM 结构。

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

定位到 Title 文本后,您可以检查问题数量

await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
何时使用 Title 定位符

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

按测试 ID 定位

通过测试 ID 进行测试是最具弹性的一种测试方式,因为即使属性的文本或角色发生变化,测试仍然会通过。质量保证人员和开发人员应该定义明确的测试 ID,并使用 Page.GetByTestId() 来查询它们。然而,按测试 ID 进行测试并不是面向用户的。如果角色或文本值对您很重要,那么考虑使用面向用户的定位符,例如角色定位符文本定位符

例如,考虑以下 DOM 结构。

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

您可以根据元素的测试 ID 进行定位

await Page.GetByTestId("directions").ClickAsync();
何时使用 Test ID 定位符

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

设置自定义测试 ID 属性

默认情况下,Page.GetByTestId() 将根据 data-testid 属性定位元素,但您可以在测试配置中或通过调用 Selectors.SetTestIdAttribute() 进行配置。

设置测试 ID,为您的测试使用自定义数据属性。

playwright.Selectors.SetTestIdAttribute("data-pw");

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

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

然后像平常一样定位元素

await Page.GetByTestId("directions").ClickAsync();

按 CSS 或 XPath 定位

如果您绝对必须使用 CSS 或 XPath 定位符,可以使用 Page.Locator() 创建一个定位符,该定位符接受一个描述如何在页面中查找元素的选择器。Playwright 支持 CSS 和 XPath 选择器,如果您省略 css=xpath= 前缀,Playwright 会自动检测它们。

await Page.Locator("css=button").ClickAsync();
await Page.Locator("xpath=//button").ClickAsync();

await Page.Locator("button").ClickAsync();
await Page.Locator("//button").ClickAsync();

XPath 和 CSS 选择器可能与 DOM 结构或实现紧密耦合。当 DOM 结构发生变化时,这些选择器可能会失效。下面的长 CSS 或 XPath 链是导致测试不稳定的不良实践的例子

await Page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input").ClickAsync();

await Page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync();
何时使用此方法

不建议使用 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").ClickAsync();
<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", new() { HasText = "Details" })
.ClickAsync();
<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")).ToContainTextAsync("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(AriaRole.Listitem)
.Filter(new() { HasText = "Product 2" })
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

使用正则表达式

await page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasTextRegex = new Regex("Product 2") })
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

按不包含文本过滤

或者,按不包含文本过滤

// 5 in-stock items
await Expect(Page.getByRole(AriaRole.Listitem).Filter(new() { HasNotText = "Out of stock" }))
.ToHaveCountAsync(5);

按子元素/后代元素过滤

定位符支持一个选项,只选择具有或不具有匹配另一个定位符的后代元素的元素。因此,您可以使用任何其他定位符进行过滤,例如 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(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.Heading, new() {
Name = "Product 2"
})
})
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

我们还可以断言产品卡片,确保只有一个

await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);

过滤定位符必须相对于原始定位符,并且从原始定位符匹配的元素开始查询,而不是从文档根部开始。因此,以下代码将无法工作,因为过滤定位符从 <ul> 列表元素开始匹配,该元素位于原始定位符匹配的 <li> 列表项之外

// ✖ WRONG
await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.List).GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);

按不包含子元素/后代元素过滤

我们还可以按不包含内部匹配元素进行过滤。

await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
HasNot = page.GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);

请注意,内部定位符是从外部定位符开始匹配的,而不是从文档根部开始。

定位符操作符

在定位符内部匹配

您可以将创建定位符的方法(如 Page.GetByText()Locator.GetByRole())链式调用,以将搜索范围缩小到页面的特定部分。

在此示例中,我们首先通过定位角色为 listitem 创建一个名为 product 的定位符。然后按文本进行过滤。我们可以再次使用 product 定位符按按钮的角色获取并点击它,然后使用断言来确保只有一个文本为 "Product 2" 的产品。

var product = page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasText = "Product 2" });

await product
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

您也可以将两个定位符链式调用,例如在特定的对话框中查找 "Save" 按钮

var saveButton = page.GetByRole(AriaRole.Button, new() { Name = "Save" });
// ...
var dialog = page.GetByTestId("settings-dialog");
await dialog.Locator(saveButton).ClickAsync();

同时匹配两个定位符

方法 Locator.And() 通过匹配附加的定位符来缩小现有定位符的范围。例如,您可以结合使用 Page.GetByRole()Page.GetByTitle() 来同时按角色和 title 进行匹配。

var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));

匹配两个备选定位符中的一个

如果您想定位两个或更多元素中的一个,并且不知道是哪一个,可以使用 Locator.Or() 创建一个匹配任意一个或两个备选定位符的定位符。

例如,考虑一种场景,您想点击“新邮件”按钮,但有时会弹出一个安全设置对话框。在这种情况下,您可以等待“新邮件”按钮或对话框出现,并根据情况采取相应的行动。

注意

如果“新邮件”按钮和安全对话框同时出现在屏幕上,“or”定位符将匹配它们两者,可能会抛出“严格模式违规”错误。在这种情况下,您可以使用 Locator.First 来只匹配其中一个。

var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
var dialog = page.GetByText("Confirm security settings");
await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync();
if (await dialog.IsVisibleAsync())
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync();
await newEmail.ClickAsync();

仅匹配可见元素

注意

通常最好找到一种更可靠的方法来唯一标识元素,而不是检查可见性。

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

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

    await page.Locator("button").ClickAsync();
  • 这将只找到第二个按钮,因为它可见,然后点击它。

    await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();

列表

统计列表中的项目数

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

例如,考虑以下 DOM 结构

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

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

await Expect(Page.GetByRole(AriaRole.Listitem)).ToHaveCountAsync(3);

断言列表中的所有文本

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

例如,考虑以下 DOM 结构

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

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

await Expect(Page
.GetByRole(AriaRole.Listitem))
.ToHaveTextAsync(new string[] {"apple", "banana", "orange"});

获取特定项目

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

按文本获取

使用 Page.GetByText() 方法根据列表项的文本内容定位列表中的元素,然后点击它。

例如,考虑以下 DOM 结构

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

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

await page.GetByText("orange").ClickAsync();

按文本过滤

使用 Locator.Filter() 定位列表中的特定项目。

例如,考虑以下 DOM 结构

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

根据角色“listitem”定位项目,然后按文本“orange”过滤,然后点击它。

await page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasText = "orange" })
.ClickAsync();

按测试 ID 获取

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

例如,考虑以下 DOM 结构

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

按第 N 个项目获取

如果您有一列相同的元素,并且区分它们的唯一方法是顺序,您可以使用 Locator.FirstLocator.LastLocator.Nth() 从列表中选择特定元素。

var banana = await page.GetByRole(AriaRole.Listitem).Nth(1);

但是,请谨慎使用此方法。页面经常可能发生变化,定位符可能会指向与您预期完全不同的元素。相反,尝试找到一个唯一的定位符,它将通过严格性标准

链式过滤

当您有具有各种相似性的元素时,可以使用 Locator.Filter() 方法选择正确的元素。您也可以链式调用多个过滤器来缩小选择范围。

例如,考虑以下 DOM 结构

http://localhost: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”和“说再见”的行进行截图

var rowLocator = page.GetByRole(AriaRole.Listitem);

await rowLocator
.Filter(new() { HasText = "Mary" })
.Filter(new() {
Has = page.GetByRole(AriaRole.Button, new() { Name = "Say goodbye" })
})
.ScreenshotAsync(new() { Path = "screenshot.png" });

现在您的项目根目录中应该有一个“screenshot.png”文件。

罕见用例

对列表中的每个元素执行操作

迭代元素

foreach (var row in await page.GetByRole(AriaRole.Listitem).AllAsync())
Console.WriteLine(await row.TextContentAsync());

使用常规 for 循环迭代

var rows = page.GetByRole(AriaRole.Listitem);
var count = await rows.CountAsync();
for (int i = 0; i < count; ++i)
Console.WriteLine(await rows.Nth(i).TextContentAsync());

在页面中评估

Locator.EvaluateAllAsync() 中的代码在页面中运行,您可以在其中调用任何 DOM API。

var rows = page.GetByRole(AriaRole.Listitem);
var texts = await rows.EvaluateAllAsync(
"list => list.map(element => element.textContent)");

严格性

定位符是严格的。这意味着如果多个元素匹配,则所有暗示某个目标 DOM 元素的定位符操作都将抛出异常。例如,如果 DOM 中有多个按钮,以下调用将抛出异常

如果多于一个则抛出错误

await page.GetByRole(AriaRole.Button).ClickAsync();

另一方面,Playwright 理解何时执行多元素操作,因此当定位符解析为多个元素时,以下调用可以正常工作。

适用于多个元素

await page.GetByRole(AriaRole.Button).CountAsync();

您可以通过 Locator.FirstLocator.LastLocator.Nth() 明确退出严格性检查,告诉 Playwright 在多个元素匹配时使用哪个元素。不推荐使用这些方法,因为当您的页面发生变化时,Playwright 可能会点击您不希望点击的元素。相反,请遵循上述最佳实践来创建一个唯一标识目标元素的定位符。

更多定位符

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