跳转到主要内容

定位器

简介

定位器是 Playwright 自动等待和重试功能的核心。简而言之,定位器表示在任何时候查找页面上元素的方法。

快速指南

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

page.getByLabel("User Name").fill("John");

page.getByLabel("Password").fill("secret-password");

page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"))
.click();

assertThat(page.getByText("Welcome, John!")).isVisible();

定位元素

Playwright 附带了多个内置定位器。为了使测试具有弹性,我们建议优先使用用户可见属性和显式契约,例如 Page.getByRole()

例如,考虑以下 DOM 结构。

https://:3000
<button>Sign in</button>

通过其 button 角色和名称“登录”定位元素。

page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"))
.click();
注意

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

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

Locator locator = page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Sign in"));

locator.hover();
locator.click();

请注意,所有创建定位器的方法,例如 Page.getByLabel(),也可用于 定位器FrameLocator 类,因此您可以将它们链式调用并迭代地缩小定位器范围。

Locator locator = page
.frameLocator("#my-frame")
.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"));

locator.click();

按角色定位

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

例如,考虑以下 DOM 结构。

https://:3000

注册

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

您可以按其隐式角色定位每个元素

assertThat(page
.getByRole(AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Sign up")))
.isVisible();

page.getByRole(AriaRole.CHECKBOX,
new Page.GetByRoleOptions().setName("Subscribe"))
.check();

page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName(
Pattern.compile("submit", Pattern.CASE_INSENSITIVE)))
.click();

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

请注意,角色定位器不替代辅助功能审计和符合性测试,而是提供有关 ARIA 指南的早期反馈。

何时使用角色定位器

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

按标签定位

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

例如,考虑以下 DOM 结构。

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

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

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

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

按占位符定位

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

例如,考虑以下 DOM 结构。

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

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

page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
何时使用占位符定位器

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

按文本定位

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

例如,考虑以下 DOM 结构。

https://:3000
欢迎,约翰
<span>Welcome, John</span>

您可以按元素包含的文本定位它

assertThat(page.getByText("Welcome, John")).isVisible();

设置精确匹配

assertThat(page
.getByText("Welcome, John", new Page.GetByTextOptions().setExact(true)))
.isVisible();

使用正则表达式匹配

assertThat(page
.getByText(Pattern.compile("welcome, john$", Pattern.CASE_INSENSITIVE)))
.isVisible();
注意

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

何时使用文本定位器

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

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

按 alt 文本定位

所有图像都应该有一个描述图像的 alt 属性。您可以使用 Page.getByAltText() 根据替代文本定位图像。

例如,考虑以下 DOM 结构。

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

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

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

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

按 title 定位

使用 Page.getByTitle() 查找具有匹配 title 属性的元素。

例如,考虑以下 DOM 结构。

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

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

assertThat(page.getByTitle("Issues count")).hasText("25 issues");
何时使用 title 定位器

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

按 test id 定位

通过测试 ID 进行测试是最具弹性的测试方式,因为即使文本或属性角色发生变化,测试仍将通过。QA 和开发人员应该定义显式的测试 ID,并使用 Page.getByTestId() 查询它们。但是,通过测试 ID 进行测试并非用户可见。如果角色或文本值对您很重要,请考虑使用面向用户的定位器,例如角色文本定位器

例如,考虑以下 DOM 结构。

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

您可以按其测试 ID 定位元素

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

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

设置自定义 test id 属性

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

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

playwright.selectors().setTestIdAttribute("data-pw");

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

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

然后像通常一样定位元素

page.getByTestId("directions").click();

按 CSS 或 XPath 定位

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

page.locator("css=button").click();
page.locator("xpath=//button").click();

page.locator("button").click();
page.locator("//button").click();

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

page.locator(
"#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input"
).click();

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 Root 不存在一样进行定位。

点击 <div>Details</div>

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>

page.locator("x-details", new Page.LocatorOptions().setHasText("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> 包含文本“详细信息”

assertThat(page.locator("x-details")).containsText("Details");

过滤定位器

考虑以下 DOM 结构,我们想点击第二个产品卡片的购买按钮。我们有几种选择来过滤定位器以获得正确的定位器。

https://: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() 方法按文本进行过滤。它会在元素内部的某个位置(可能在后代元素中)搜索特定字符串,不区分大小写。您还可以传递正则表达式。

page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("Product 2"))
.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Add to cart"))
.click();

使用正则表达式

page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHasText(Pattern.compile("Product 2")))
.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Add to cart"))
.click();

按不包含文本过滤

或者,按不包含文本进行过滤

// 5 in-stock items
assertThat(page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasNotText("Out of stock")))
.hasCount(5);

按子/后代过滤

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

https://: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>
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHas(page.GetByRole(AriaRole.HEADING, new Page.GetByRoleOptions()
.setName("Product 2"))))
.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Add to cart"))
.click();

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

assertThat(page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHas(page.GetByRole(AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Product 2")))))
.hasCount(1);

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

// ✖ WRONG
assertThat(page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHas(page.GetByRole(AriaRole.LIST)
.GetByRole(AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Product 2")))))
.hasCount(1);

按不包含子/后代过滤

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

assertThat(page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasNot(page.getByText("Product 2"))))
.hasCount(1);

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

定位器操作符

在定位器内部匹配

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

在这个例子中,我们首先通过定位其 listitem 角色创建一个名为 product 的定位器。然后我们按文本进行过滤。我们可以再次使用 product 定位器通过按钮角色并点击它,然后使用断言确保只有一个文本为“产品 2”的产品。

Locator product = page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("Product 2"));

product
.getByRole(AriaRole.BUTTON,
new Locator.GetByRoleOptions().setName("Add to cart"))
.click();

您还可以将两个定位器链式调用在一起,例如在特定对话框中查找“保存”按钮

Locator saveButton = page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Save"));
// ...
Locator dialog = page.getByTestId("settings-dialog");
dialog.locator(saveButton).click();

同时匹配两个定位器

方法 Locator.and() 通过匹配附加定位器来缩小现有定位器。例如,您可以组合 Page.getByRole()Page.getByTitle(),以通过角色和标题进行匹配。

Locator button = page.getByRole(AriaRole.BUTTON).and(page.getByTitle("Subscribe"));

匹配两个或多个备用定位器中的一个

如果您想定位两个或多个元素中的一个,并且不知道是哪一个,请使用 Locator.or() 创建一个可以匹配其中任何一个或所有替代项的定位器。

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

注意

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

Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
Locator dialog = page.getByText("Confirm security settings");
assertThat(newEmail.or(dialog).first()).isVisible();
if (dialog.isVisible())
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
newEmail.click();

只匹配可见元素

注意

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

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

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

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

    page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click();

列表

统计列表中的项目数

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

例如,考虑以下 DOM 结构

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

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

assertThat(page.getByRole(AriaRole.LISTITEM)).hasCount(3);

断言列表中所有文本

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

例如,考虑以下 DOM 结构

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

使用 assertThat(locator).hasText() 确保列表包含文本“apple”、“banana”和“orange”。

assertThat(page
.getByRole(AriaRole.LISTITEM))
.hasText(new String[] { "apple", "banana", "orange" });

获取特定项目

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

按文本获取

使用 Page.getByText() 方法通过文本内容定位列表中的元素,然后单击它。

例如,考虑以下 DOM 结构

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

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

page.getByText("orange").click();

按文本过滤

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

例如,考虑以下 DOM 结构

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

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

page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("orange"))
.click();

按 test id 获取

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

例如,考虑以下 DOM 结构

https://:3000
  • 苹果
  • 香蕉
  • 橙子
<ul>
<li data-testid='apple'>apple</li>
<li data-testid='banana'>banana</li>
<li data-testid='orange'>orange</li>
</ul>

按其测试 ID“orange”定位项目,然后点击它。

page.getByTestId("orange").click();

按第 n 项获取

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

Locator banana = page.getByRole(AriaRole.LISTITEM).nth(1);

但是,请谨慎使用此方法。通常情况下,页面可能会更改,并且定位器将指向与您预期完全不同的元素。相反,尝试提出一个独特的定位器,该定位器将通过严格性标准

链式过滤器

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

例如,考虑以下 DOM 结构

https://: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>

截取带有“玛丽”和“说再见”的行的屏幕截图

Locator rowLocator = page.getByRole(AriaRole.LISTITEM);

rowLocator
.filter(new Locator.FilterOptions().setHasText("Mary"))
.filter(new Locator.FilterOptions()
.setHas(page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Say goodbye"))))
.screenshot(new Page.ScreenshotOptions().setPath("screenshot.png"));

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

不常见的用例

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

迭代元素

for (Locator row : page.getByRole(AriaRole.LISTITEM).all())
System.out.println(row.textContent());

使用常规 for 循环迭代

Locator rows = page.getByRole(AriaRole.LISTITEM);
int count = rows.count();
for (int i = 0; i < count; ++i)
System.out.println(rows.nth(i).textContent());

在页面中评估

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

Locator rows = page.getByRole(AriaRole.LISTITEM);
Object texts = rows.evaluateAll(
"list => list.map(element => element.textContent)");

严格性

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

如果多于一个则抛出错误

page.getByRole(AriaRole.BUTTON).click();

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

对多个元素正常工作

page.getByRole(AriaRole.BUTTON).count();

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

更多定位器

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