跳到主内容

定位符

引言

定位符是 Playwright 自动等待和可重试性的核心。简单来说,定位符代表了在任何时刻找到页面上元素的一种方式。

快速指南

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

page.get_by_label("User Name").fill("John")

page.get_by_label("Password").fill("secret-password")

page.get_by_role("button", name="Sign in").click()

expect(page.get_by_text("Welcome, John!")).to_be_visible()

定位元素

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

例如,考虑以下 DOM 结构。

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

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

page.get_by_role("button", name="Sign in").click()
注意

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

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

locator = page.get_by_role("button", name="Sign in")

locator.hover()
locator.click()

请注意,所有创建定位符的方法,例如 page.get_by_label(),在 LocatorFrameLocator 类上也可用,因此您可以链式调用它们并迭代地缩小定位范围。

locator = page.frame_locator("my-frame").get_by_role("button", name="Sign in")

locator.click()

按角色定位

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

例如,考虑以下 DOM 结构。

http://localhost:3000

Sign up

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

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

expect(page.get_by_role("heading", name="Sign up")).to_be_visible()

page.get_by_role("checkbox", name="Subscribe").check()

page.get_by_role("button", name=re.compile("submit", re.IGNORECASE)).click()

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

请注意,角色定位符不能替代可访问性审计和一致性测试,但可以提供关于 ARIA 指南的早期反馈。

何时使用角色定位符

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

按标签定位

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

例如,考虑以下 DOM 结构。

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

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

page.get_by_label("Password").fill("secret")
何时使用标签定位符

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

按占位符定位

输入框可能具有 placeholder 属性,以向用户提示应输入的值。您可以使用 page.get_by_placeholder() 定位此类输入框。

例如,考虑以下 DOM 结构。

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

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

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

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

按文本定位

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

例如,考虑以下 DOM 结构。

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

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

expect(page.get_by_text("Welcome, John")).to_be_visible()

设置精确匹配

expect(page.get_by_text("Welcome, John", exact=True)).to_be_visible()

使用正则表达式匹配

expect(page.get_by_text(re.compile("welcome, john", re.IGNORECASE))).to_be_visible()
注意

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

何时使用文本定位符

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

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

按 alt 文本定位

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

例如,考虑以下 DOM 结构。

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

通过文本替代属性定位图像后,您可以点击它

page.get_by_alt_text("playwright logo").click()
何时使用 alt 定位符

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

按 title 定位

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

例如,考虑以下 DOM 结构。

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

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

expect(page.get_by_title("Issues count")).to_have_text("25 issues")
何时使用 title 定位符

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

按测试 ID 定位

按测试 ID 进行测试是最有弹性的测试方法,因为即使您的文本或属性角色发生变化,测试仍然会通过。质量保证人员和开发人员应该定义显式的测试 ID,并使用 page.get_by_test_id() 查询它们。然而,按测试 ID 进行测试并非面向用户。如果您认为角色或文本值很重要,请考虑使用面向用户的定位符,例如 角色文本定位符

例如,考虑以下 DOM 结构。

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

您可以通过其测试 ID 定位元素

page.get_by_test_id("directions").click()
何时使用 testid 定位符

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

设置自定义测试 ID 属性

默认情况下,page.get_by_test_id() 将基于 data-testid 属性定位元素,但您可以在测试配置中或通过调用 selectors.set_test_id_attribute() 进行配置。

设置测试 ID 以使用自定义 data 属性进行测试。

playwright.selectors.set_test_id_attribute("data-pw")

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

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

然后像通常一样定位元素

page.get_by_test_id("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()
何时使用此方法

不建议使用 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.get_by_text("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", has_text="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"

expect(page.locator("x-details")).to_contain_text("Details")

过滤定位符

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

http://localhost:3000
  • Product 1

  • Product 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.get_by_role("listitem").filter(has_text="Product 2").get_by_role(
"button", name="Add to cart"
).click()

使用正则表达式

page.get_by_role("listitem").filter(has_text=re.compile("Product 2")).get_by_role(
"button", name="Add to cart"
).click()

过滤不含指定文本的元素

或者,过滤不含指定文本的元素

# 5 in-stock items
expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)

按子元素/后代元素过滤

定位符支持仅选择包含或不包含匹配另一个定位符的后代的元素。因此,您可以通过任何其他定位符进行过滤,例如 locator.get_by_role()locator.get_by_test_id()locator.get_by_text() 等。

http://localhost:3000
  • Product 1

  • Product 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.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
).get_by_role("button", name="Add to cart").click()

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

expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
)
).to_have_count(1)

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

# ✖ WRONG
expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("list").get_by_role("heading", name="Product 2")
)
).to_have_count(1)

过滤不含指定子元素/后代元素的元素

我们还可以过滤不包含匹配元素的元素。

expect(
page.get_by_role("listitem").filter(
has_not=page.get_by_role("heading", name="Product 2")
)
).to_have_count(1)

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

定位符操作符

在定位符内部匹配

您可以链式调用创建定位符的方法,例如 page.get_by_text()locator.get_by_role(),以将搜索范围缩小到页面的特定部分。

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

product = page.get_by_role("listitem").filter(has_text="Product 2")

product.get_by_role("button", name="Add to cart").click()

您也可以将两个定位符链式连接起来,例如在特定的对话框中查找“保存”按钮

save_button = page.get_by_role("button", name="Save")
# ...
dialog = page.get_by_test_id("settings-dialog")
dialog.locator(save_button).click()

同时匹配两个定位符

locator.and_() 方法通过匹配额外的定位符来缩小现有定位符的范围。例如,您可以结合 page.get_by_role()page.get_by_title() 同时按角色和 title 进行匹配。

button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))

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

如果您想定位两个或更多元素中的一个,但不知道是哪一个,请使用 locator.or_() 创建一个匹配任一或全部备选定位符的定位符。

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

注意

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

new_email = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm security settings")
expect(new_email.or_(dialog).first).to_be_visible()
if (dialog.is_visible()):
page.get_by_role("button", name="Dismiss").click()
new_email.click()

仅匹配可见元素

注意

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

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

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

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

    page.locator("button").filter(visible=True).click()

列表

统计列表中的项目数

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

例如,考虑以下 DOM 结构

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

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

expect(page.get_by_role("listitem")).to_have_count(3)

断言列表中的所有文本

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

例如,考虑以下 DOM 结构

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

使用 expect(locator).to_have_text() 确保列表包含文本 "apple"、"banana" 和 "orange"。

expect(page.get_by_role("listitem")).to_have_text(["apple", "banana", "orange"])

获取特定项目

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

按文本获取

使用 page.get_by_text() 方法按文本内容定位列表中的元素,然后点击它。

例如,考虑以下 DOM 结构

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

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

page.get_by_text("orange").click()

按文本过滤

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

例如,考虑以下 DOM 结构

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

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

page.get_by_role("listitem").filter(has_text="orange").click()

按测试 ID 获取

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

例如,考虑以下 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”定位项目,然后点击它。

page.get_by_test_id("orange").click()

按第 n 个项目获取

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

banana = page.get_by_role("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”和“Say goodbye”那一行元素的屏幕截图

row_locator = page.get_by_role("listitem")

row_locator.filter(has_text="Mary").filter(
has=page.get_by_role("button", name="Say goodbye")
).screenshot(path="screenshot.png")

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

罕见用例

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

迭代元素

for row in page.get_by_role("listitem").all():
print(row.text_content())

使用常规 for 循环迭代

rows = page.get_by_role("listitem")
count = rows.count()
for i in range(count):
print(rows.nth(i).text_content())

在页面中评估

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

rows = page.get_by_role("listitem")
texts = rows.evaluate_all("list => list.map(element => element.textContent)")

严格性

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

匹配多个时抛出错误

page.get_by_role("button").click()

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

匹配多个时正常工作

page.get_by_role("button").count()

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

更多定位符

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