跳到主要内容

定位器

简介

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

快速指南

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

await Page.GetByLabel("用户名").FillAsync("John");

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

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

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

定位元素

Playwright 提供了多种内置定位器。为了使测试更具弹性,我们建议优先使用面向用户的属性和明确的约定,例如 Page.GetByRole()

例如,考虑以下 DOM 结构。

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

通过其 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>注册</h3>
<label>
<input type="checkbox" /> 订阅
</label>
<br/>
<button>提交</button>

你可以按每个元素的隐式角色来定位它们:

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

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

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

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

请注意,角色定位器 不能替代 可访问性审核和一致性测试,而是提供有关 ARIA 指南的早期反馈。

何时使用角色定位器

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

通过标签定位

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

例如,考虑以下 DOM 结构。

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

你可以在通过标签文本定位到输入框后填充内容:

await Page.GetByLabel("密码").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
Welcome, 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 text)定位

所有图像都应具有描述图像的 alt 属性。你可以使用 Page.GetByAltText() 基于替代文本定位图像。

例如,考虑以下 DOM 结构。

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

你可以在通过替代文本定位到图像后点击它:

await Page.GetByAltText("Playwright 徽标").ClickAsync();
何时使用替代文本定位器

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

通过标题定位

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

例如,考虑以下 DOM 结构。

http://localhost:3000
25 个问题
<span title='问题数量'>25 个问题</span>

你可以在通过标题文本定位到元素后检查问题数量:

await Expect(Page.GetByTitle("问题数量")).toHaveText("25 个问题");
何时使用标题定位器

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

通过测试 ID 定位

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

例如,考虑以下 DOM 结构。

http://localhost:3000
<button data-testid="directions">路线</button>

你可以通过测试 ID 定位该元素:

await Page.GetByTestId("directions").ClickAsync();
何时使用测试 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">路线</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>

你可以像影子根根本不存在一样进行定位。

要点击 <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>产品 1</h3>
<button>加入购物车</button>
</li>
<li>
<h3>产品 2</h3>
<button>加入购物车</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 个有货商品
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>产品 1</h3>
<button>加入购物车</button>
</li>
<li>
<h3>产品 2</h3>
<button>加入购物车</button>
</li>
</ul>
await page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.Heading, new() {
Name = "产品 2"
})
})
.GetByRole(AriaRole.Button, new() { Name = "加入购物车" })
.ClickAsync();

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

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

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

// ✖ 错误
await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.List).GetByRole(AriaRole.Heading, new() { Name = "产品 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();

你也可以将两个定位器链接在一起,例如在特定对话框中查找 "保存" 按钮:

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() 结合起来,同时根据角色和标题进行匹配。

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

匹配两个可选定位器中的一个

如果您想要定位两个或更多元素中的一个,并且不知道具体是哪一个,可以使用 Locator.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
  • apple
  • banana
  • orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

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

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

断言列表中的所有文本

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

例如,考虑以下 DOM 结构:

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

使用 Expect(Locator).ToHaveTextAsync() 来确保列表中有文本 "apple"、"banana" 和 "orange"。

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

获取特定项目

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

按文本获取

使用 Page.GetByText() 方法按文本内容在列表中定位一个元素,然后点击它。

例如,考虑以下 DOM 结构:

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

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

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

按文本筛选

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

例如,考虑以下 DOM 结构:

http://localhost:3000
  • apple
  • banana
  • orange
<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
  • 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").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>打招呼</button></div>
</li>
<li>
<div>Mary</div>
<div><button>打招呼</button></div>
</li>
<li>
<div>John</div>
<div><button>说再见</button></div>
</li>
<li>
<div>Mary</div>
<div><button>说再见</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 = "说再见" })
})
.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 可能会点击你不想要的元素。相反,应遵循上述最佳实践,创建一个能唯一标识目标元素的定位器。

更多定位器

有关不太常用的定位器,请参阅 其他定位器 指南。