定位器
简介
Locator是Playwright自动等待和重试能力的核心部分。简而言之,定位器代表了一种随时在页面上查找元素的方法。
快速指南
以下是推荐的内置定位器。
- Page.GetByRole() 通过显式和隐式可访问性属性进行定位。
- Page.GetByText() 通过文本内容定位。
- Page.GetByLabel() 通过关联标签的文本来定位表单控件。
- Page.GetByPlaceholder() 通过占位符定位输入框。
- Page.GetByAltText() 通过替代文本来定位元素(通常是图像)。
- Page.GetByTitle() 通过元素的title属性来定位元素。
- Page.GetByTestId() 根据元素的
data-testid
属性来定位元素(可以配置其他属性)。
await Page.GetByLabel("User Name").FillAsync("John");
await Page.GetByLabel("Password").FillAsync("secret-password");
await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
await Expect(Page.GetByText("Welcome, John!")).ToBeVisibleAsync();
定位元素
Playwright 内置了多种定位器。为了使测试更加健壮,我们建议优先使用面向用户的属性和显式约定,例如 Page.GetByRole()。
例如,考虑以下DOM结构。
<button>Sign in</button>
通过其角色为button
且名称为"Sign in"来定位该元素。
await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
使用代码生成器生成定位器,然后根据需要编辑它。
每次使用定位器执行操作时,都会在页面中定位最新的DOM元素。在下面的代码片段中,底层DOM元素将被定位两次,每次操作前各一次。这意味着如果在调用之间由于重新渲染导致DOM发生变化,将使用与定位器对应的新元素。
var locator = Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" });
await locator.HoverAsync();
await locator.ClickAsync();
请注意,所有创建定位器的方法,例如 Page.GetByLabel(),在 Locator 和 FrameLocator 类中也都可用,因此您可以链式调用它们并逐步缩小定位范围。
var locator = Page
.FrameLocator("#my-frame")
.GetByRole(AriaRole.Button, new() { Name = "Sign in" });
await locator.ClickAsync();
按角色定位
Page.GetByRole()定位器反映了用户和辅助技术对页面的感知方式,例如某个元素是按钮还是复选框。当通过角色定位时,通常还应传递可访问名称,以便定位器能精确定位到具体元素。
例如,考虑以下DOM结构。
注册
<h3>Sign up</h3>
<label>
<input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>
您可以通过元素的隐式角色来定位每个元素:
await Expect(Page
.GetByRole(AriaRole.Heading, new() { Name = "Sign up" }))
.ToBeVisibleAsync();
await Page
.GetByRole(AriaRole.Checkbox, new() { Name = "Subscribe" })
.CheckAsync();
await Page
.GetByRole(AriaRole.Button, new() {
NameRegex = new Regex("submit", RegexOptions.IgnoreCase)
})
.ClickAsync();
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 <button>
have an implicitly defined role that is recognized by the role locator.
请注意,角色定位器并不能替代可访问性审计和合规性测试,而是提供关于ARIA指南的早期反馈。
我们建议优先使用角色定位器来定位元素,因为这是最接近用户和辅助技术感知页面的方式。
通过标签定位
大多数表单控件通常都有专门的标签,可以方便地用于与表单交互。在这种情况下,您可以使用Page.GetByLabel()通过关联标签来定位控件。
例如,考虑以下DOM结构。
<label>Password <input type="password" /></label>
您可以通过标签文本定位后填写输入框:
await Page.GetByLabel("Password").FillAsync("secret");
在定位表单字段时使用此定位器。
通过占位符定位
输入框可能带有placeholder属性,用于提示用户应输入的值。您可以使用Page.GetByPlaceholder()来定位此类输入框。
例如,考虑以下DOM结构。
<input type="email" placeholder="name@example.com" />
您可以通过占位符文本定位后填写输入框:
await Page
.GetByPlaceholder("name@example.com")
.FillAsync("playwright@microsoft.com");
当定位没有标签但具有占位符文本的表单元素时,请使用此定位器。
通过文本定位
通过元素包含的文本查找元素。在使用Page.GetByText()时,您可以通过子字符串、精确字符串或正则表达式进行匹配。
例如,考虑以下DOM结构。
<span>Welcome, John</span>
您可以通过元素包含的文本来定位该元素:
await Expect(Page.GetByText("Welcome, John")).ToBeVisibleAsync();
设置完全匹配:
await Expect(Page
.GetByText("Welcome, John", new() { Exact = true }))
.ToBeVisibleAsync();
使用正则表达式匹配:
await Expect(Page
.GetByText(new Regex("welcome, john", RegexOptions.IgnoreCase)))
.ToBeVisibleAsync();
按文本匹配时总是会规范化空白字符,即使是精确匹配。例如,它会将多个空格转换为一个,将换行符转换为空格,并忽略开头和结尾的空白字符。
我们建议使用文本定位器来查找非交互式元素,如div
、span
、p
等。对于交互式元素如button
、a
、input
等,请使用role locators。
您还可以按文本筛选,这在尝试查找列表中的特定项目时非常有用。
通过alt文本定位
所有图片都应包含描述图片内容的alt
属性。您可以使用Page.GetByAltText()通过替代文本来定位图片。
例如,考虑以下DOM结构。
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
您可以通过替代文本定位后点击图像:
await Page.GetByAltText("playwright logo").ClickAsync();
当您的元素支持替代文本(如img
和area
元素)时,请使用此定位器。
通过标题定位
使用Page.GetByTitle()定位具有匹配title属性的元素。
例如,考虑以下DOM结构。
<span title='Issues count'>25 issues</span>
您可以通过标题文本定位后检查问题数量:
await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
当您的元素具有title
属性时,请使用此定位器。
通过测试ID定位
通过测试ID进行测试是最稳健的测试方式,因为即使属性文本或角色发生变化,测试仍能通过。质量保证人员和开发人员应定义明确的测试ID,并使用Page.GetByTestId()来查询它们。然而,基于测试ID的测试并不面向最终用户。如果角色或文本值对您很重要,那么请考虑使用面向用户的定位器,如role和text locators。
例如,考虑以下DOM结构。
<button data-testid="directions">Itinéraire</button>
您可以通过元素的测试ID来定位它:
await Page.GetByTestId("directions").ClickAsync();
设置自定义测试ID属性
默认情况下,Page.GetByTestId()会基于data-testid
属性定位元素,但您可以在测试配置中或通过调用Selectors.SetTestIdAttribute()来配置它。
设置测试ID以便在测试中使用自定义数据属性。
playwright.Selectors.SetTestIdAttribute("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>
然后像平常一样定位元素:
await Page.GetByTestId("directions").ClickAsync();
通过CSS或XPath定位
如果您确实必须使用CSS或XPath定位器,可以使用Page.Locator()创建一个定位器,该定位器接收描述如何在页面中查找元素的选择器。Playwright支持CSS和XPath选择器,如果您省略css=
或xpath=
前缀,它会自动检测类型。
await Page.Locator("css=button").ClickAsync();
await Page.Locator("xpath=//button").ClickAsync();
await Page.Locator("button").ClickAsync();
await Page.Locator("//button").ClickAsync();
XPath和CSS选择器可能会与DOM结构或实现方式绑定。当DOM结构发生变化时,这些选择器可能会失效。下方展示的长CSS或XPath链就是导致测试不稳定的不良实践示例:
await Page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input").ClickAsync();
await Page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync();
在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一样进行定位。
点击 <div>Details</div>
:
await page.GetByText("Details").ClickAsync();
<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>
:
await page
.Locator("x-details", new() { HasText = "Details" })
.ClickAsync();
<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":
await Expect(Page.Locator("x-details")).ToContainTextAsync("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()方法按文本进行过滤。它会在元素内部某处(可能在子元素中)搜索特定字符串,不区分大小写。您也可以传递正则表达式。
await page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasText = "Product 2" })
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();
使用正则表达式:
await page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasTextRegex = new Regex("Product 2") })
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();
按不包含文本筛选
或者,通过不包含文本来筛选:
// 5 in-stock items
await Expect(Page.getByRole(AriaRole.Listitem).Filter(new() { HasNotText = "Out of stock" }))
.ToHaveCountAsync(5);
按子节点/后代节点筛选
定位器(Locator)支持一个选项,可以仅选择具有或不具有匹配另一个定位器的后代元素的元素。因此,您可以通过任何其他定位器进行筛选,例如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>
await page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.Heading, new() {
Name = "Product 2"
})
})
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();
我们也可以断言产品卡片以确保只有一个:
await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);
过滤定位器必须是相对于原始定位器的,并且查询是从原始定位器匹配项开始的,而不是从文档根目录开始的。因此,以下方法将不起作用,因为过滤定位器是从<ul>
列表元素开始匹配的,而该元素位于原始定位器匹配的<li>
列表项之外:
// ✖ WRONG
await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.List).GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);
按不包含子/后代元素筛选
我们还可以通过不包含内部匹配元素来进行筛选。
await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
HasNot = page.GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);
请注意,内部定位器是从外部定位器开始匹配的,而不是从文档根节点开始。
定位器操作符
在定位器内部匹配
你可以链式调用创建定位器的方法,比如Page.GetByText()或Locator.GetByRole(),来将搜索范围缩小到页面的特定部分。
在这个示例中,我们首先通过定位其listitem
角色创建一个名为product的定位器。然后我们通过文本进行筛选。我们可以再次使用product定位器来获取按钮角色并点击它,然后使用断言来确保只有一个文本为"Product 2"的产品。
var product = page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasText = "Product 2" });
await product
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();
你也可以将两个定位器串联使用,例如在特定对话框内查找"保存"按钮:
var saveButton = page.GetByRole(AriaRole.Button, new() { Name = "Save" });
// ...
var dialog = page.GetByTestId("settings-dialog");
await dialog.Locator(saveButton).ClickAsync();
同时匹配两个定位器
方法 Locator.And() 通过匹配额外的定位器来缩小现有定位器的范围。例如,您可以组合 Page.GetByRole() 和 Page.GetByTitle() 来同时匹配角色和标题。
var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));
匹配两个备选定位器中的一个
如果您想定位两个或多个元素中的一个,但不确定具体是哪一个,可以使用Locator.Or()创建一个匹配任意一个或两个备选项的定位器。
例如,考虑这样一个场景:您想要点击"新建邮件"按钮,但有时会弹出一个安全设置对话框。在这种情况下,您可以等待"新建邮件"按钮或对话框出现,然后采取相应的操作。
如果"新邮件"按钮和安全对话框同时出现在屏幕上,"or"定位器会匹配两者,可能会抛出"严格模式违规"错误。这种情况下,您可以使用Locator.First来仅匹配其中一个元素。
var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
var dialog = page.GetByText("Confirm security settings");
await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync();
if (await dialog.IsVisibleAsync())
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync();
await newEmail.ClickAsync();
仅匹配可见元素
通常来说,寻找一种更可靠的方式来唯一标识元素比检查可见性更好。
考虑一个页面有两个按钮,第一个不可见而第二个可见。
<button style='display: none'>Invisible</button>
<button>Visible</button>
-
这将找到两个按钮并抛出strictness违反错误:
await page.Locator("button").ClickAsync();
-
这只会找到第二个按钮,因为它是可见的,然后点击它。
await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();
列表
计算列表中的项目数量
你可以断言定位器来统计列表中的项目数量。
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
使用count断言来确保列表中有3个项目。
await Expect(Page.GetByRole(AriaRole.Listitem)).ToHaveCountAsync(3);
断言列表中的所有文本
您可以通过断言定位器来查找列表中的所有文本。
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
使用 Expect(Locator).ToHaveTextAsync() 来确保列表中包含文本"apple"、"banana"和"orange"。
await Expect(Page
.GetByRole(AriaRole.Listitem))
.ToHaveTextAsync(new string[] {"apple", "banana", "orange"});
获取特定项目
有多种方法可以获取列表中的特定项。
通过文本获取
使用Page.GetByText()方法通过文本内容定位列表中的元素,然后点击它。
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
通过文本内容定位一个项目并点击它。
await page.GetByText("orange").ClickAsync();
按文本筛选
使用 Locator.Filter() 来定位列表中的特定项目。
例如,考虑以下DOM结构:
- apple
- banana
- 橙子
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
通过角色"listitem"定位一个项目,然后通过文本"orange"进行筛选,最后点击它。
await page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasText = "orange" })
.ClickAsync();
通过测试ID获取
Use the Page.GetByTestId() 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"定位一个项目,然后点击它。
await page.GetByTestId("orange").ClickAsync();
获取第N项
如果您有一系列相同的元素,并且区分它们的唯一方式是顺序,您可以使用Locator.First、Locator.Last或Locator.Nth()从列表中选择特定元素。
var banana = await page.GetByRole(AriaRole.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"这一行的屏幕截图:
var rowLocator = page.GetByRole(AriaRole.Listitem);
await rowLocator
.Filter(new() { HasText = "Mary" })
.Filter(new() {
Has = page.GetByRole(AriaRole.Button, new() { Name = "Say goodbye" })
})
.ScreenshotAsync(new() { Path = "screenshot.png" });
您现在应该在项目的根目录下有一个名为"screenshot.png"的文件。
罕见用例
对列表中的每个元素执行操作
遍历元素:
foreach (var row in await page.GetByRole(AriaRole.Listitem).AllAsync())
Console.WriteLine(await row.TextContentAsync());
使用常规for循环进行迭代:
var rows = page.GetByRole(AriaRole.Listitem);
var count = await rows.CountAsync();
for (int i = 0; i < count; ++i)
Console.WriteLine(await rows.Nth(i).TextContentAsync());
在页面中评估
Locator.EvaluateAllAsync()中的代码在页面内运行,您可以在其中调用任何DOM API。
var rows = page.GetByRole(AriaRole.Listitem);
var texts = await rows.EvaluateAllAsync(
"list => list.map(element => element.textContent)");
严格模式
定位器(Locators)是严格的。这意味着如果匹配到多个DOM元素,所有基于定位器的操作(暗示需要特定目标DOM元素)都会抛出异常。例如,当DOM中存在多个按钮时,以下调用会抛出异常:
如果超过一个元素则抛出错误
await page.GetByRole(AriaRole.Button).ClickAsync();
另一方面,Playwright能够识别您执行的多元素操作,因此当定位器解析为多个元素时,以下调用可以完美运行。
与多个元素配合良好
await page.GetByRole(AriaRole.Button).CountAsync();
您可以通过指定Playwright在多个元素匹配时使用哪个元素来显式退出严格性检查,方法是使用Locator.First、Locator.Last和Locator.Nth()。这些方法不推荐使用,因为当您的页面发生变化时,Playwright可能会点击您不想要的元素。相反,请遵循上述最佳实践来创建一个能唯一标识目标元素的定位器。
更多定位器
对于较少使用的定位器,请查看其他定位器指南。