Locator 定位器
介绍
Locator 是 Playwright 自动等待和重试能力的核心部分。简而言之,定位器表示一种在任何时刻在页面上找到元素的方法。
快速指南
以下是推荐的内置定位器。
- page.get_by_role() 根据显式和隐式的可访问性属性进行定位。
- page.get_by_text() 根据文本内容进行定位。
- page.get_by_label() 根据关联标签的文本定位表单控件。
- page.get_by_placeholder() 根据占位符定位输入框。
- page.get_by_alt_text() 根据文本替代内容定位元素,通常是图像。
- page.get_by_title() 根据元素的 title 属性进行定位。
- page.get_by_test_id() 根据元素的
data-testid
属性进行定位(可以配置其他属性)。
- 同步
- 异步
page.get_by_label("用户名").fill("John")
page.get_by_label("密码").fill("secret-password")
page.get_by_role("button", name="登录").click()
expect(page.get_by_text("欢迎, John!")).to_be_visible()
await page.get_by_label("用户名").fill("John")
await page.get_by_label("密码").fill("secret-password")
await page.get_by_role("button", name="登录").click()
await expect(page.get_by_text("欢迎, John!")).to_be_visible()
定位元素
Playwright 提供了多种内置定位器。为了使测试更具弹性,我们建议优先使用面向用户的属性和显式约定,例如 page.get_by_role()。
例如,考虑以下 DOM 结构。
<button>登录</button>
通过其角色 button
和名称 "登录" 定位元素。
- 同步
- 异步
page.get_by_role("button", name="登录").click()
await page.get_by_role("button", name="登录").click()
使用 代码生成器 来生成定位器,然后根据需要进行编辑。
每次定位器用于执行操作时,页面中都会定位到最新的 DOM 元素。在下面的代码片段中,底层 DOM 元素会被定位两次,每次操作之前都会定位一次。这意味着如果在调用之间由于重新渲染导致 DOM 发生变化,将使用与定位器对应的新元素。
- 同步
- 异步
locator = page.get_by_role("button", name="登录")
locator.hover()
locator.click()
locator = page.get_by_role("button", name="登录")
await locator.hover()
await locator.click()
请注意,所有创建定位器的方法,例如 page.get_by_label(),也可以在 Locator 和 FrameLocator 类上使用,因此您可以链式调用它们并逐步缩小定位范围。
- 同步
- 异步
locator = page.frame_locator("my-frame").get_by_role("button", name="登录")
locator.click()
locator = page.frame_locator("#my-frame").get_by_role("button", name="登录")
await locator.click()
根据角色定位
page.get_by_role() 定位器反映了用户和辅助技术如何感知页面,例如某个元素是按钮还是复选框。在根据角色定位时,通常应同时传递可访问名称,以便定位器能够精确定位到具体的元素。
例如,考虑以下 DOM 结构。
注册
<h3>注册</h3>
<label>
<input type="checkbox" /> 订阅
</label>
<br/>
<button>提交</button>
您可以通过其隐式角色定位每个元素:
- 同步
- 异步
expect(page.get_by_role("heading", name="注册")).to_be_visible()
page.get_by_role("checkbox", name="订阅").check()
page.get_by_role("button", name=re.compile("提交", re.IGNORECASE)).click()
await expect(page.get_by_role("heading", name="注册")).to_be_visible()
await page.get_by_role("checkbox", name="订阅").check()
await page.get_by_role("button", name=re.compile("提交", re.IGNORECASE)).click()
角色定位器包括按钮、复选框、标题、链接、列表、表格等,并遵循 W3C 关于ARIA 角色、ARIA 属性和可访问名称的规范。请注意,许多 HTML 元素(例如 <button>
)具有隐式定义的角色,这些角色会被角色定位器识别。
请注意,角色定位器并不能替代无障碍性审查和合规性测试,但可以对 ARIA 指南提供早期反馈。
我们建议优先使用角色定位器来定位元素,因为这是最接近用户和辅助技术感知页面的方式。
根据标签定位
大多数表单控件通常都有专用的标签,可以方便地用于与表单交互。在这种情况下,您可以使用 page.get_by_label() 根据其关联的标签定位控件。
例如,考虑以下 DOM 结构。
<label>密码 <input type="password" /></label>
您可以通过标签文本定位输入框并填充内容:
- 同步
- 异步
page.get_by_label("密码").fill("secret")
await page.get_by_label("密码").fill("secret")
当定位表单字段时使用此定位器。
根据占位符定位
输入框可能具有占位符属性,用于提示用户应输入的值。您可以使用 page.get_by_placeholder() 定位这样的输入框。
例如,考虑以下 DOM 结构。
<input type="email" placeholder="name@example.com" />
您可以通过占位符文本定位输入框并填充内容:
- 同步
- 异步
page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com")
await page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com")
当定位没有标签但有占位符文本的表单元素时使用此定位器。
根据文本定位
通过元素包含的文本查找元素。您可以使用 page.get_by_text() 按子字符串、精确字符串或正则表达式进行匹配。
例如,考虑以下 DOM 结构。
<span>欢迎, John</span>
您可以通过包含的文本定位元素:
- 同步
- 异步
expect(page.get_by_text("欢迎, John")).to_be_visible()
await expect(page.get_by_text("欢迎, John")).to_be_visible()
设置精确匹配:
- 同步
- 异步
expect(page.get_by_text("欢迎, John", exact=True)).to_be_visible()
await expect(page.get_by_text("欢迎, John", exact=True)).to_be_visible()
使用正则表达式匹配:
- 同步
- 异步
expect(page.get_by_text(re.compile("欢迎, john", re.IGNORECASE))).to_be_visible()
await expect(
page.get_by_text(re.compile("欢迎, john", re.IGNORECASE))
).to_be_visible()
通过文本匹配时总是会规范化空白,即使是精确匹配。例如,它会将多个空格变为一个,将换行符变为空格,并忽略前后的空白。
我们建议使用文本定位器来查找非交互式元素,例如 div
、span
、p
等。对于交互式元素,例如 button
、a
、input
等,请使用角色定位器。
您还可以通过文本过滤,这在尝试查找列表中特定项目时非常有用。
根据替代文本定位
所有图像都应具有描述图像的 alt
属性。您可以使用 page.get_by_alt_text() 根据文本替代内容定位图像。
例如,考虑以下 DOM 结构。
<img alt="playwright 徽标" src="/img/playwright-logo.svg" width="100" />
您可以通过定位文本替代内容后点击图像:
- 同步
- 异步
page.get_by_alt_text("playwright 徽标").click()
await page.get_by_alt_text("playwright 徽标").click()
当您的元素支持替代文本(例如 img
和 area
元素)时,请使用此定位器。
根据标题定位
使用 page.get_by_title() 根据匹配的 title 属性定位元素。
例如,考虑以下 DOM 结构。
<span title='问题数量'>25 个问题</span>
您可以通过定位标题文本后检查问题数量:
- 同步
- 异步
expect(page.get_by_title("问题数量")).to_have_text("25 个问题")
await expect(page.get_by_title("问题数量")).to_have_text("25 个问题")
当您的元素具有 title
属性时,请使用此定位器。
根据测试 ID 定位
通过测试 ID 进行测试是最具弹性的测试方式,即使属性的文本或角色发生变化,测试仍然可以通过。QA 和开发人员应定义明确的测试 ID,并使用 page.get_by_test_id() 查询它们。然而,通过测试 ID 进行测试并不是面向用户的。如果角色或文本值对您很重要,请考虑使用面向用户的定位器,例如角色定位器和文本定位器。
例如,考虑以下 DOM 结构。
<button data-testid="directions">路线</button>
您可以通过测试 ID 定位元素:
- 同步
- 异步
page.get_by_test_id("directions").click()
await page.get_by_test_id("directions").click()
设置自定义测试 ID 属性
默认情况下,page.get_by_test_id() 将基于 data-testid
属性定位元素,但您可以在测试配置中进行配置,或者通过调用 selectors.set_test_id_attribute() 来设置。
设置测试 ID 以便在测试中使用自定义数据属性。
- 同步
- 异步
playwright.selectors.set_test_id_attribute("data-pw")
playwright.selectors.set_test_id_attribute("data-pw")
在您的 HTML 中,您现在可以使用 data-pw
作为测试 ID,而不是默认的 data-testid
。
<button data-pw="directions">路线</button>
然后像通常一样定位元素:
- 同步
- 异步
page.get_by_test_id("directions").click()
await 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()
await page.locator("css=button").click()
await page.locator("xpath=//button").click()
await page.locator("button").click()
await 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()
await page.locator(
"#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input"
).click()
await page.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input').click()
定位 Shadow DOM 中的元素
Playwright 中的所有定位器默认支持 Shadow DOM 中的元素。以下是例外情况:
- 使用 XPath 定位时不会穿透 shadow 根节点。
- 不支持关闭模式的 shadow 根节点。
以下是一个包含自定义 Web 组件的示例:
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>标题</div>
#shadow-root
<div id=inner-details>详情</div>
</x-details>
您可以像 shadow 根节点不存在一样进行定位。
点击 <div>详情</div>
:
- 同步
- 异步
page.get_by_text("详情").click()
await page.get_by_text("详情").click()
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>标题</div>
#shadow-root
<div id=inner-details>详情</div>
</x-details>
点击 <x-details>
:
- 同步
- 异步
page.locator("x-details", has_text="详情").click()
await page.locator("x-details", has_text="详情").click()
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>标题</div>
#shadow-root
<div id=inner-details>详情</div>
</x-details>
确保 <x-details>
包含文本 "详情":
- 同步
- 异步
expect(page.locator("x-details")).to_contain_text("详情")
await expect(page.locator("x-details")).to_contain_text("详情")
过滤定位器
考虑以下 DOM 结构,我们希望点击第二个产品卡片的购买按钮。我们有几种方法可以过滤定位器以找到正确的元素。
产品 1
产品 2
<ul>
<li>
<h3>产品 1</h3>
<button>加入购物车</button>
</li>
<li>
<h3>产品 2</h3>
<button>加入购物车</button>
</li>
</ul>
根据文本过滤
可以使用 locator.filter() 方法根据文本过滤定位器。它会在元素内部的某个位置(可能是子元素)搜索特定字符串,忽略大小写。您也可以传递正则表达式。
- 同步
- 异步
page.get_by_role("listitem").filter(has_text="产品 2").get_by_role(
"button", name="加入购物车"
).click()
await page.get_by_role("listitem").filter(has_text="产品 2").get_by_role(
"button", name="加入购物车"
).click()
使用正则表达式:
- 同步
- 异步
page.get_by_role("listitem").filter(has_text=re.compile("产品 2")).get_by_role(
"button", name="加入购物车"
).click()
await page.get_by_role("listitem").filter(has_text=re.compile("产品 2")).get_by_role(
"button", name="加入购物车"
).click()
根据不包含文本过滤
或者,可以根据不包含的文本进行过滤:
- 同步
- 异步
# 5 个有库存的商品
expect(page.get_by_role("listitem").filter(has_not_text="缺货")).to_have_count(5)
# 5 个有库存的商品
await expect(page.get_by_role("listitem").filter(has_not_text="缺货")).to_have_count(5)
根据子元素/后代元素过滤
定位器支持仅选择具有或不具有匹配另一个定位器的后代元素的选项。因此,您可以通过任何其他定位器进行过滤,例如 locator.get_by_role()、locator.get_by_test_id()、locator.get_by_text() 等。
产品 1
产品 2
<ul>
<li>
<h3>产品 1</h3>
<button>加入购物车</button>
</li>
<li>
<h3>产品 2</h3>
<button>加入购物车</button>
</li>
</ul>
- 同步
- 异步
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="产品 2")
).get_by_role("button", name="加入购物车").click()
await page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="产品 2")
).get_by_role("button", name="加入购物车").click()
我们还可以断言产品卡片以确保只有一个:
- 同步
- 异步
expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="产品 2")
)
).to_have_count(1)
await expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="产品 2")
)
).to_have_count(1)
过滤定位器必须是相对的,并从原始定位器匹配开始查询,而不是从文档根节点开始。因此,以下示例将不起作用,因为过滤定位器从 <ul>
列表元素开始匹配,而该元素位于原始定位器匹配的 <li>
列表项之外:
- 同步
- 异步
# ✖ 错误
expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("list").get_by_role("heading", name="产品 2")
)
).to_have_count(1)
# ✖ 错误
await expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("list").get_by_role("heading", name="产品 2")
)
).to_have_count(1)
根据不包含子元素/后代元素过滤
我们还可以通过不包含匹配的元素进行过滤。
- 同步
- 异步
expect(
page.get_by_role("listitem").filter(
has_not=page.get_by_role("heading", name="产品 2")
)
).to_have_count(1)
await expect(
page.get_by_role("listitem").filter(
has_not=page.get_by_role("heading", name="产品 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()
product = page.get_by_role("listitem").filter(has_text="Product 2")
await 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()
save_button = page.get_by_role("button", name="Save")
# ...
dialog = page.get_by_test_id("settings-dialog")
await dialog.locator(save_button).click()
同时匹配两个定位器
方法 locator.and_() 通过匹配一个额外的定位器来缩小现有定位器的范围。例如,您可以结合使用 page.get_by_role() 和 page.get_by_title() 来同时匹配角色和标题。
- 同步
- 异步
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
匹配两个定位器中的一个
如果您想定位两个或多个元素中的一个,但您不知道会是哪一个,可以使用 locator.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()
new_email = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm security settings")
await expect(new_email.or_(dialog).first).to_be_visible()
if (await dialog.is_visible()):
await page.get_by_role("button", name="Dismiss").click()
await new_email.click()
仅匹配可见元素
通常更好地找到一种更可靠的方法来唯一标识元素,而不是检查可见性。
考虑一个页面,其中有两个按钮,第一个是不可见的,第二个是可见的。
<button style='display: none'>Invisible</button>
<button>Visible</button>
-
这将找到两个按钮并抛出一个严格性违规错误:
- 同步
- 异步
page.locator("button").click()
await page.locator("button").click()
-
这将只找到第二个按钮,因为它是可见的,然后点击它。
- 同步
- 异步
page.locator("button").filter(visible=True).click()
await page.locator("button").filter(visible=True).click()
列表
统计列表中的项目数量
您可以通过断言定位器来统计列表中的项目数量。
例如,考虑以下 DOM 结构:
- 苹果
- 香蕉
- 橙子
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
使用计数断言确保列表中有 3 个项目。
- 同步
- 异步
expect(page.get_by_role("listitem")).to_have_count(3)
await expect(page.get_by_role("listitem")).to_have_count(3)
断言列表中的所有文本
您可以通过断言定位器来找到列表中的所有文本。
例如,考虑以下 DOM 结构:
- 苹果
- 香蕉
- 橙子
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
使用 expect(locator).to_have_text() 确保列表中包含文本 "苹果"、"香蕉" 和 "橙子"。
- 同步
- 异步
expect(page.get_by_role("listitem")).to_have_text(["苹果", "香蕉", "橙子"])
await expect(page.get_by_role("listitem")).to_have_text(["苹果", "香蕉", "橙子"])
获取特定项目
有多种方法可以获取列表中的特定项目。
通过文本获取
使用 page.get_by_text() 方法通过文本内容定位列表中的元素,然后点击它。
例如,考虑以下 DOM 结构:
- 苹果
- 香蕉
- 橙子
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
通过其文本内容定位项目并点击它。
- 同步
- 异步
page.get_by_text("橙子").click()
await page.get_by_text("橙子").click()
通过文本过滤
使用 locator.filter() 定位列表中的特定项目。
例如,考虑以下 DOM 结构:
- 苹果
- 香蕉
- 橙子
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
通过角色 "listitem" 定位项目,然后根据文本 "橙子" 进行过滤并点击它。
- 同步
- 异步
page.get_by_role("listitem").filter(has_text="橙子").click()
await page.get_by_role("listitem").filter(has_text="橙子").click()
通过测试 ID 获取
使用 page.get_by_test_id() 方法定位列表中的元素。如果您尚未添加测试 ID,可能需要修改 HTML 并添加测试 ID。
例如,考虑以下 DOM 结构:
- 苹果
- 香蕉
- 橙子
<ul>
<li data-testid='apple'>苹果</li>
<li data-testid='banana'>香蕉</li>
<li data-testid='orange'>橙子</li>
</ul>
通过测试 ID "orange" 定位项目并点击它。
- 同步
- 异步
page.get_by_test_id("orange").click()
await page.get_by_test_id("orange").click()
通过第 n 个项目获取
如果您有一组相同的元素,并且区分它们的唯一方法是顺序,您可以使用 locator.first、locator.last 或 locator.nth() 从列表中选择特定元素。
- 同步
- 异步
banana = page.get_by_role("listitem").nth(1)
banana = await page.get_by_role("listitem").nth(1)
然而,请谨慎使用此方法。页面可能会发生变化,定位器可能会指向与您预期完全不同的元素。相反,请尝试设计一个能够通过 严格性标准 的唯一定位器。
链式过滤
当您有多个相似的元素时,可以使用 locator.filter() 方法选择正确的元素。您还可以链式调用多个过滤器以缩小选择范围。
例如,考虑以下 DOM 结构:
- 约翰
- 玛丽
- 约翰
- 玛丽
<ul>
<li>
<div>约翰</div>
<div><button>打招呼</button></div>
</li>
<li>
<div>玛丽</div>
<div><button>打招呼</button></div>
</li>
<li>
<div>约翰</div>
<div><button>说再见</button></div>
</li>
<li>
<div>玛丽</div>
<div><button>说再见</button></div>
</li>
</ul>
截取包含 "玛丽" 和 "说再见" 的行的截图:
- 同步
- 异步
row_locator = page.get_by_role("listitem")
row_locator.filter(has_text="玛丽").filter(
has=page.get_by_role("button", name="说再见")
).screenshot(path="screenshot.png")
row_locator = page.get_by_role("listitem")
await row_locator.filter(has_text="玛丽").filter(
has=page.get_by_role("button", name="说再见")
).screenshot(path="screenshot.png")
现在,您的项目根目录中应该有一个名为 "screenshot.png" 的文件。
罕见的使用场景
对列表中的每个元素执行操作
迭代元素:
- 同步
- 异步
for row in page.get_by_role("listitem").all():
print(row.text_content())
for row in await page.get_by_role("listitem").all():
print(await row.text_content())
使用常规的 for 循环迭代:
- 同步
- 异步
rows = page.get_by_role("listitem")
count = rows.count()
for i in range(count):
print(rows.nth(i).text_content())
rows = page.get_by_role("listitem")
count = await rows.count()
for i in range(count):
print(await 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)")
rows = page.get_by_role("listitem")
texts = await rows.evaluate_all("list => list.map(element => element.textContent)")
严格性
定位器是严格的。这意味着所有涉及目标 DOM 元素的定位器操作,如果匹配到多个元素,将抛出异常。例如,以下调用如果 DOM 中有多个按钮会抛出错误:
如果匹配多个元素会抛出错误
- 同步
- 异步
page.get_by_role("button").click()
await page.get_by_role("button").click()
另一方面,当您执行多元素操作时,Playwright 能够理解,因此以下调用在定位器解析为多个元素时可以正常工作。
对多个元素操作正常工作
- 同步
- 异步
page.get_by_role("button").count()
await page.get_by_role("button").count()
您可以通过 locator.first、locator.last 和 locator.nth() 明确选择 Playwright 在匹配多个元素时使用哪个元素,从而显式地取消严格性检查。这些方法不推荐使用,因为当页面发生变化时,Playwright 可能会点击您未预期的元素。相反,请遵循上述最佳实践,创建唯一标识目标元素的定位器。
更多定位器
对于不常用的定位器,请参阅其他定位器指南。