定位器
引言
定位器 是 Playwright 自动等待和重试能力的核心。简而言之,定位器表示在任何时刻查找页面上元素的一种方式。
快速指南
以下是推荐的内置定位器。
- Page.getByRole() 根据显式和隐式可访问性属性进行定位。
- Page.getByText() 根据文本内容进行定位。
- Page.getByLabel() 根据关联标签的文本定位表单控件。
- Page.getByPlaceholder() 根据占位符定位输入框。
- Page.getByAltText() 根据文本替代内容定位元素(通常是图像)。
- Page.getByTitle() 根据 title 属性定位元素。
- Page.getByTestId() 根据元素的
data-testid
属性进行定位(可以配置其他属性)。
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 结构。
<button>Sign in</button>
通过 role 为 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(),在 Locator 和 FrameLocator 类上也都可用,因此您可以将它们链式调用并迭代地缩小定位器范围。
Locator locator = page
.frameLocator("#my-frame")
.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"));
locator.click();
按角色定位
Page.getByRole() 定位器反映了用户和辅助技术如何感知页面,例如某个元素是按钮还是复选框。按角色定位时,您通常还应传递可访问名称,以便定位器精确定位到目标元素。
例如,考虑以下 DOM 结构。
注册
<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 结构。
<label>Password <input type="password" /></label>
通过标签文本定位输入框后,您可以填充它
page.getByLabel("Password").fill("secret");
定位表单字段时使用此定位器。
按占位符定位
输入框可能具有 placeholder 属性,用于提示用户应输入什么值。您可以使用 Page.getByPlaceholder() 定位此类输入框。
例如,考虑以下 DOM 结构。
<input type="email" placeholder="name@example.com" />
通过占位符文本定位输入框后,您可以填充它
page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
定位没有标签但有占位符文本的表单元素时使用此定位器。
按文本定位
根据元素包含的文本查找元素。使用 Page.getByText() 时,您可以按子字符串、精确字符串或正则表达式进行匹配。
例如,考虑以下 DOM 结构。
<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();
通过文本匹配始终会规范化空白字符,即使是精确匹配也是如此。例如,它会将多个空格变成一个,将换行符变成空格,并忽略前导和尾随的空白字符。
我们建议使用文本定位器查找非交互式元素,如 div
、span
、p
等。对于交互式元素,如 button
、a
、input
等,请使用 角色定位器。
您还可以按文本筛选,这在尝试查找列表中的特定项目时非常有用。
按 Alt 文本定位
所有图像都应具有描述图像的 alt
属性。您可以使用 Page.getByAltText() 根据文本替代内容定位图像。
例如,考虑以下 DOM 结构。
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
通过文本替代内容定位图像后,您可以点击它
page.getByAltText("playwright logo").click();
当您的元素支持 alt 文本(例如 img
和 area
元素)时使用此定位器。
按标题定位
使用 Page.getByTitle() 定位具有匹配 title 属性的元素。
例如,考虑以下 DOM 结构。
<span title='Issues count'>25 issues</span>
通过 title 文本定位后,您可以检查问题数量
assertThat(page.getByTitle("Issues count")).hasText("25 issues");
当您的元素具有 title
属性时使用此定位器。
按测试 ID 定位
按测试 ID 进行测试是最具弹性的测试方法,因为即使属性的文本或角色发生变化,测试仍然会通过。质量保证人员和开发人员应定义明确的测试 ID,并使用 Page.getByTestId() 查询它们。然而,按测试 ID 进行测试不是面向用户的。如果角色或文本值对您很重要,那么请考虑使用面向用户的定位器,例如角色和文本定位器。
例如,考虑以下 DOM 结构。
<button data-testid="directions">Itinéraire</button>
您可以通过元素的测试 ID 定位它
page.getByTestId("directions").click();
设置自定义测试 ID 属性
默认情况下,Page.getByTestId() 将基于 data-testid
属性定位元素,但您可以在测试配置中或通过调用 Selectors.setTestIdAttribute() 来配置它。
设置测试 ID 以在测试中使用自定义数据属性。
playwright.selectors().setTestIdAttribute("data-pw");
现在您可以在 HTML 中使用 data-pw
作为测试 ID,而不是默认的 data-testid
。
<button data-pw="directions">Itinéraire</button>
然后像往常一样定位元素
page.getByTestId("directions").click();
按 CSS 或 XPath 定位
如果您必须使用 CSS 或 XPath 定位器,可以使用 Page.locator() 创建一个接受描述如何在页面中查找元素的选择器的定位器。Playwright 支持 CSS 和 XPath 选择器,如果您省略 css=
或 xpath=
前缀,它会自动检测。
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();
在 Shadow DOM 中定位
默认情况下,Playwright 中的所有定位器都能与 Shadow DOM 中的元素一起工作。例外情况是
- 按 XPath 定位不能穿透 Shadow Root。
- 不支持封闭模式的 Shadow Root。
考虑以下使用自定义 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>
包含文本“Details”
assertThat(page.locator("x-details")).containsText("Details");
筛选定位器
考虑以下 DOM 结构,我们想点击第二个产品卡片的购买按钮。我们有几种选项来筛选定位器以找到正确的按钮。
产品 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() 等。
产品 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(),将搜索范围缩小到页面的特定部分。
在此示例中,我们首先通过定位 role 为 listitem
创建一个名为 product 的定位器。然后我们按文本进行筛选。我们可以再次使用 product 定位器来获取 role 为 button 的元素并点击它,然后使用断言确保只有一个文本为“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() 创建一个匹配任一备选或两者都匹配的定位器。
例如,考虑一种情况,您想点击“新邮件”按钮,但有时会弹出安全设置对话框。在这种情况下,您可以等待“新邮件”按钮或对话框,并采取相应的行动。
如果“新邮件”按钮和安全对话框都出现在屏幕上,“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 结构
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
使用 count 断言来确保列表有 3 个项目。
assertThat(page.getByRole(AriaRole.LISTITEM)).hasCount(3);
断言列表中的所有文本
您可以断言定位器以查找列表中的所有文本。
例如,考虑以下 DOM 结构
- apple
- banana
- orange
<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 结构
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
根据项目文本内容定位并点击它。
page.getByText("orange").click();
按文本筛选
使用 Locator.filter() 在列表中定位特定项目。
例如,考虑以下 DOM 结构
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
通过 role 为“listitem”定位项目,然后按文本“orange”筛选,然后点击它。
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("orange"))
.click();
按测试 ID 获取
使用 Page.getByTestId() 方法在列表中定位元素。如果还没有测试 ID,您可能需要修改 HTML 并添加一个。
例如,考虑以下 DOM 结构
- 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”定位项目,然后点击它。
page.getByTestId("orange").click();
按第 N 个项目获取
如果您有一个包含相同元素的列表,并且区分它们唯一的方法是顺序,您可以使用 Locator.first()、Locator.last() 或 Locator.nth() 从列表中选择特定元素。
Locator banana = page.getByRole(AriaRole.LISTITEM).nth(1);
然而,请谨慎使用此方法。页面经常会发生变化,定位器可能会指向与您预期完全不同的元素。相反,尝试使用能够满足严格性标准的唯一定位器。
链式筛选器
当您拥有具有各种相似性的元素时,可以使用 Locator.filter() 方法选择正确的元素。您也可以链式调用多个筛选器以缩小选择范围。
例如,考虑以下 DOM 结构
- 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”和“说再见”的行的屏幕截图
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 可能会点击到您不期望的元素。相反,请遵循上述最佳实践,创建一个唯一标识目标元素的定位器。
更多定位器
对于不常用的定位器,请查阅其他定位器指南。