定位器
简介
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
属性定位元素(可以配置其他属性)。
- Sync
- 异步
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()
await page.get_by_label("User Name").fill("John")
await page.get_by_label("Password").fill("secret-password")
await page.get_by_role("button", name="Sign in").click()
await expect(page.get_by_text("Welcome, John!")).to_be_visible()
定位元素
Playwright 内置了多种定位器。为了使测试更加健壮,我们建议优先使用面向用户的属性和显式契约,例如 page.get_by_role()。
例如,考虑以下DOM结构。
<button>Sign in</button>
通过其角色为button
且名称为"Sign in"来定位该元素。
- Sync
- 异步
page.get_by_role("button", name="Sign in").click()
await page.get_by_role("button", name="Sign in").click()
使用代码生成器生成定位器,然后根据需要编辑它。
每次使用定位器执行操作时,都会在页面中定位最新的DOM元素。在下面的代码片段中,底层DOM元素将被定位两次,每次操作前各一次。这意味着如果在调用之间由于重新渲染导致DOM发生变化,将使用与定位器对应的新元素。
- Sync
- 异步
locator = page.get_by_role("button", name="Sign in")
locator.hover()
locator.click()
locator = page.get_by_role("button", name="Sign in")
await locator.hover()
await locator.click()
请注意,所有创建定位器的方法,例如page.get_by_label(),在Locator和FrameLocator类上也都可用,因此您可以链式调用它们并逐步缩小定位范围。
- Sync
- 异步
locator = page.frame_locator("my-frame").get_by_role("button", name="Sign in")
locator.click()
locator = page.frame_locator("#my-frame").get_by_role("button", name="Sign in")
await locator.click()
按角色定位
page.get_by_role()定位器反映了用户和辅助技术如何感知页面,例如某个元素是按钮还是复选框。当通过角色定位时,通常还应传递可访问名称,以便定位器精确定位到具体元素。
例如,考虑以下DOM结构。
注册
<h3>Sign up</h3>
<label>
<input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>
您可以通过元素的隐式角色来定位每个元素:
- Sync
- 异步
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()
await expect(page.get_by_role("heading", name="Sign up")).to_be_visible()
await page.get_by_role("checkbox", name="Subscribe").check()
await page.get_by_role("button", name=re.compile("submit", re.IGNORECASE)).click()
Role locators include buttons, checkboxes, headings, links, lists, tables, and many more and follow W3C specifications for ARIA role, ARIA attributes and accessible name. Note that many html elements like have an implicitly defined role that is recognized by the role locator.
请注意,角色定位器并不能替代可访问性审计和合规性测试,而是提供关于ARIA指南的早期反馈。
我们建议优先使用角色定位器来定位元素,因为这是最接近用户和辅助技术感知页面的方式。
通过标签定位
大多数表单控件通常都有专门的标签,可以方便地用于与表单交互。在这种情况下,您可以使用page.get_by_label()通过关联标签来定位控件。
例如,考虑以下DOM结构。
<label>Password <input type="password" /></label>
您可以通过标签文本定位后填写输入框:
- Sync
- 异步
page.get_by_label("Password").fill("secret")
await page.get_by_label("Password").fill("secret")
在定位表单字段时使用此定位器。
通过占位符定位
输入框可能有一个占位符属性,用于提示用户应输入什么值。您可以使用page.get_by_placeholder()来定位这样的输入框。
例如,考虑以下DOM结构。
<input type="email" placeholder="name@example.com" />
您可以通过占位符文本定位后填写输入框:
- Sync
- 异步
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>Welcome, John</span>
您可以通过元素包含的文本来定位该元素:
- Sync
- 异步
expect(page.get_by_text("Welcome, John")).to_be_visible()
await expect(page.get_by_text("Welcome, John")).to_be_visible()
设置完全匹配:
- Sync
- 异步
expect(page.get_by_text("Welcome, John", exact=True)).to_be_visible()
await expect(page.get_by_text("Welcome, John", exact=True)).to_be_visible()
使用正则表达式匹配:
- Sync
- 异步
expect(page.get_by_text(re.compile("welcome, john", re.IGNORECASE))).to_be_visible()
await expect(
page.get_by_text(re.compile("welcome, john", re.IGNORECASE))
).to_be_visible()
按文本匹配时总是会规范化空白字符,即使是精确匹配。例如,它会将多个空格转换为一个,将换行符转换为空格,并忽略开头和结尾的空白字符。
我们建议使用文本定位器来查找非交互式元素,如div
、span
、p
等。对于交互式元素如button
、a
、input
等,请使用role locators。
您还可以按文本筛选,这在尝试查找列表中的特定项目时非常有用。
通过alt文本定位
所有图片都应包含一个描述图片内容的alt
属性。您可以使用page.get_by_alt_text()通过替代文本来定位图片。
例如,考虑以下DOM结构。
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
您可以通过替代文本定位后点击图像:
- Sync
- 异步
page.get_by_alt_text("playwright logo").click()
await page.get_by_alt_text("playwright logo").click()
当您的元素支持替代文本(如img
和area
元素)时,请使用此定位器。
通过标题定位
使用page.get_by_title()定位具有匹配title属性的元素。
例如,考虑以下DOM结构。
<span title='Issues count'>25 issues</span>
您可以通过标题文本定位后检查问题数量:
- Sync
- 异步
expect(page.get_by_title("Issues count")).to_have_text("25 issues")
await expect(page.get_by_title("Issues count")).to_have_text("25 issues")
当您的元素具有title
属性时,请使用此定位器。
通过测试ID定位
通过测试ID进行测试是最稳健的测试方式,因为即使属性文本或角色发生变化,测试仍能通过。质量保证人员和开发人员应定义明确的测试ID,并使用page.get_by_test_id()来查询它们。然而,通过测试ID进行的测试并不面向用户。如果角色或文本值对您很重要,那么考虑使用面向用户的定位器,如role和text locators。
例如,考虑以下DOM结构。
<button data-testid="directions">Itinéraire</button>
您可以通过元素的测试ID来定位它:
- Sync
- 异步
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以便在测试中使用自定义数据属性。
- Sync
- 异步
playwright.selectors.set_test_id_attribute("data-pw")
playwright.selectors.set_test_id_attribute("data-pw")
In your html you can now use data-pw
as your test id instead of the default data-testid
.
<button data-pw="directions">Itinéraire</button>
然后像平常一样定位元素:
- Sync
- 异步
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=
前缀,它会自动检测它们。
- Sync
- 异步
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链就是导致测试不稳定的不良实践示例:
- Sync
- 异步
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根节点。
- Closed-mode shadow roots 不受支持。
考虑以下带有自定义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一样进行定位。
点击
:
- Sync
- 异步
page.get_by_text("Details").click()
await 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>
要点击
:
- Sync
- 异步
page.locator("x-details", has_text="Details").click()
await 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>
确保
包含文本"Details":
- Sync
- 异步
expect(page.locator("x-details")).to_contain_text("Details")
await expect(page.locator("x-details")).to_contain_text("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()方法按文本进行过滤。它会在元素内部某处(可能在子元素中)不区分大小写地搜索特定字符串。您也可以传递正则表达式。
- Sync
- 异步
page.get_by_role("listitem").filter(has_text="Product 2").get_by_role(
"button", name="Add to cart"
).click()
await page.get_by_role("listitem").filter(has_text="Product 2").get_by_role(
"button", name="Add to cart"
).click()
使用正则表达式:
- Sync
- 异步
page.get_by_role("listitem").filter(has_text=re.compile("Product 2")).get_by_role(
"button", name="Add to cart"
).click()
await page.get_by_role("listitem").filter(has_text=re.compile("Product 2")).get_by_role(
"button", name="Add to cart"
).click()
按不包含文本筛选
或者,通过不包含文本来筛选:
- Sync
- 异步
# 5 in-stock items
expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)
# 5 in-stock items
await expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)
按子节点/后代节点筛选
定位器(Locator)支持一个选项,可以仅选择具有或不具有匹配另一个定位器的后代元素。因此您可以通过任何其他定位器进行筛选,例如locator.get_by_role()、locator.get_by_test_id()、locator.get_by_text()等。
产品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>
- Sync
- 异步
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
).get_by_role("button", name="Add to cart").click()
await page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
).get_by_role("button", name="Add to cart").click()
我们也可以断言产品卡片以确保只有一个:
- Sync
- 异步
expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
)
).to_have_count(1)
await expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("heading", name="Product 2")
)
).to_have_count(1)
过滤定位器必须是相对于原始定位器的,并且查询是从原始定位器匹配项开始的,而不是从文档根目录开始的。因此,以下方法将不起作用,因为过滤定位器是从
列表元素开始匹配的,而该元素位于原始定位器匹配的列表项之外:
- Sync
- 异步
# ✖ 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)
# ✖ WRONG
await expect(
page.get_by_role("listitem").filter(
has=page.get_by_role("list").get_by_role("heading", name="Product 2")
)
).to_have_count(1)
按不包含子/后代元素筛选
我们还可以通过不包含内部匹配元素来进行筛选。
- Sync
- 异步
expect(
page.get_by_role("listitem").filter(
has_not=page.get_by_role("heading", name="Product 2")
)
).to_have_count(1)
await 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"的产品。
- Sync
- 异步
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()
你也可以将两个定位器串联使用,例如在特定对话框内查找"保存"按钮:
- Sync
- 异步
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() 来同时匹配角色和标题。
- Sync
- 异步
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
button = page.get_by_role("button").and_(page.getByTitle("Subscribe"))
匹配两个备选定位器中的一个
如果您想定位两个或多个元素中的某一个,但不确定具体是哪一个,可以使用locator.or_()创建一个匹配其中任意一个或两个替代项的定位器。
例如,考虑这样一个场景:您想要点击"新建邮件"按钮,但有时会弹出一个安全设置对话框。在这种情况下,您可以等待"新建邮件"按钮或对话框出现,然后采取相应的操作。
如果屏幕上同时出现"新邮件"按钮和安全对话框,"或"定位器将匹配两者,可能会抛出"严格模式违规"错误。在这种情况下,您可以使用locator.first仅匹配其中一个。
- Sync
- 异步
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>
-
这将找到两个按钮并抛出strictness违反错误:
- Sync
- 异步
page.locator("button").click()
await page.locator("button").click()
-
这将仅找到第二个按钮,因为它是可见的,然后点击它。
- Sync
- 异步
page.locator("button").filter(visible=True).click()
await page.locator("button").filter(visible=True).click()
列表
计算列表中的项目数量
你可以断言定位器来统计列表中的项目数量。
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
使用count断言来确保列表中有3个项目。
- Sync
- 异步
expect(page.get_by_role("listitem")).to_have_count(3)
await expect(page.get_by_role("listitem")).to_have_count(3)
断言列表中的所有文本
您可以通过断言定位器来查找列表中的所有文本。
例如,考虑以下DOM结构:
- apple
- banana
- 橙色
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
使用 expect(locator).to_have_text() 确保列表包含文本"apple"、"banana"和"orange"。
- Sync
- 异步
expect(page.get_by_role("listitem")).to_have_text(["apple", "banana", "orange"])
await expect(page.get_by_role("listitem")).to_have_text(["apple", "banana", "orange"])
获取特定项目
有多种方法可以获取列表中的特定项。
通过文本获取
使用page.get_by_text()方法通过文本内容定位列表中的元素,然后点击它。
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
通过文本内容定位一个项目并点击它。
- Sync
- 异步
page.get_by_text("orange").click()
await page.get_by_text("orange").click()
按文本筛选
使用 locator.filter() 在列表中定位特定项目。
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
通过角色"listitem"定位一个项目,然后通过文本"orange"进行筛选,最后点击它。
- Sync
- 异步
page.get_by_role("listitem").filter(has_text="orange").click()
await page.get_by_role("listitem").filter(has_text="orange").click()
通过测试ID获取
Use the page.get_by_test_id() method to locate an element in a list. You may need to modify the html and add a test id if you don't already have a test id.
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li data-testid='apple'>apple</li>
<li data-testid='banana'>banana</li>
<li data-testid='orange'>orange</li>
</ul>
通过测试ID为"orange"定位一个项目,然后点击它。
- Sync
- 异步
page.get_by_test_id("orange").click()
await page.get_by_test_id("orange").click()
获取第N项
如果您有一组相同的元素,并且区分它们的唯一方法是顺序,您可以使用locator.first、locator.last或locator.nth()从列表中选择特定元素。
- Sync
- 异步
banana = page.get_by_role("listitem").nth(1)
banana = await page.get_by_role("listitem").nth(1)
然而,请谨慎使用此方法。很多时候,页面可能会发生变化,定位器会指向与您预期完全不同的元素。相反,尝试提出一个能通过严格标准的唯一定位器。
链式过滤器
当元素具有多种相似特征时,您可以使用locator.filter()方法来选择正确的元素。您还可以链接多个过滤器来缩小选择范围。
例如,考虑以下DOM结构:
- 约翰
- 玛丽
- 约翰
- 玛丽
<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"这一行的屏幕截图:
- Sync
- 异步
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")
row_locator = page.get_by_role("listitem")
await row_locator.filter(has_text="Mary").filter(
has=page.get_by_role("button", name="Say goodbye")
).screenshot(path="screenshot.png")
您现在应该在项目的根目录下有一个名为"screenshot.png"的文件。
罕见用例
对列表中的每个元素执行操作
遍历元素:
- Sync
- 异步
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循环进行迭代:
- Sync
- 异步
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。
- Sync
- 异步
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)")
严格模式
定位器(Locators)是严格的。这意味着如果匹配到多个DOM元素,所有基于定位器的操作(暗示需要特定目标DOM元素)都会抛出异常。例如,当DOM中存在多个按钮时,以下调用会抛出异常:
如果超过一个元素则抛出错误
- Sync
- 异步
page.get_by_role("button").click()
await page.get_by_role("button").click()
另一方面,Playwright能够识别您执行的多元素操作,因此当定位器解析为多个元素时,以下调用可以完美运行。
与多个元素配合良好
- Sync
- 异步
page.get_by_role("button").count()
await page.get_by_role("button").count()
您可以通过指定Playwright在多个元素匹配时使用哪个元素来明确选择退出严格性检查,通过locator.first、locator.last和locator.nth()。这些方法不推荐使用,因为当您的页面发生变化时,Playwright可能会点击您不想要的元素。相反,请遵循上述最佳实践来创建一个唯一标识目标元素的定位器。
更多定位器
对于较少使用的定位器,请查看其他定位器指南。