Playwright 第16章:定位器(Locators)
定位器(Locators)是 Playwright 自动化测试的核心概念。它是查找和操作页面元素的统一 API,相比传统的 DOM 操作,定位器具有自动等待、重试机制和更好的可读性等优势。本章将全面介绍 Playwright 的各种定位策略。
定位器概念
什么是定位器
定位器是 Playwright 中用于在页面上查找元素的核心抽象。与传统的 elementHandle 或 document.querySelector 不同,定位器具有以下特性:
- 自动等待:在操作元素前自动等待元素可见、可用
- 自动重试:元素未出现时自动重试,直到超时
- 严格性检查:默认要求定位器只能匹配一个元素
- 可组合性:支持链式组合和过滤
import { test, expect } from '@playwright/test';
test('定位器基本使用', async ({ page }) => {
await page.goto('https://example.com');
// 创建定位器(不会立即查询 DOM)
const button = page.locator('.submit-button');
// 执行操作(触发自动等待和重试)
await button.click();
// 断言(同样具有自动重试机制)
await expect(button).toBeVisible();
});
定位器 vs 传统选择器
| 特性 | 定位器(Locator) | 传统选择器($eval/$) |
|---|---|---|
| 自动等待 | 是 | 否 |
| 重试机制 | 内置 | 需手动实现 |
| 严格性 | 默认严格 | 不严格(返回第一个) |
| 可组合 | 支持链式调用 | 不支持 |
| 可读性 | 好 | 一般 |
| 调试 | 丰富错误信息 | 基础错误信息 |
page.locator() 方法
page.locator() 是 Playwright 中最核心的定位方法,接收一个选择器字符串并返回定位器实例:
// 基本用法
const locator = page.locator('selector');
// 选择器可以是 CSS、XPath 或其他内置定位策略
const byCSS = page.locator('.class-name');
const byXPath = page.locator('//div[@id="main"]');
const byText = page.locator('text=登录');
const byRole = page.locator('role=button[name="提交"]');
// 定位器支持链式调用
await page.locator('.form').locator('input#username').fill('test');
定位器的操作
const locator = page.locator('.button');
// 获取元素数量
const count = await locator.count();
// 获取文本内容
const text = await locator.textContent();
// 获取属性值
const href = await locator.getAttribute('href');
// 判断元素是否存在
const exists = await locator.isVisible();
// 获取所有匹配元素
const allTexts = await locator.allTextContents();
const allInnerTexts = await locator.allInnerTexts();
定位器的严格性
Playwright 默认要求定位器只能匹配一个元素。如果匹配到多个元素,操作时会抛出错误:
// 页面有多个 .item 时会报错
await page.locator('.item').click(); // Error: strict mode violation
// 使用 first()、last() 或 nth() 解决
await page.locator('.item').first().click();
await page.locator('.item').last().click();
await page.locator('.item').nth(2).click();
// 或者禁用严格性(不推荐)
await page.locator('.item').click({ strict: false });
CSS 选择器
CSS 选择器是最基础也最常用的定位方式,Playwright 支持所有标准 CSS 选择器:
// 标签选择器
page.locator('button');
page.locator('input');
page.locator('div');
// 类选择器
page.locator('.btn-primary');
page.locator('.header .nav-link');
// ID 选择器
page.locator('#username');
page.locator('#submit-btn');
// 属性选择器
page.locator('[data-type="submit"]');
page.locator('[name="email"]');
page.locator('[aria-label="关闭"]');
page.locator('[href^="https://"]'); // 以 https:// 开头
page.locator('[href$=".pdf"]'); // 以 .pdf 结尾
page.locator('[class*="btn-"]'); // 包含 btn-
// 组合选择器
page.locator('div.form-group > input[type="text"]');
page.locator('ul.nav li.active a');
// 伪类选择器
page.locator('button:disabled');
page.locator('li:first-child');
page.locator('li:nth-child(3)');
page.locator('input:not([disabled])');
Playwright 专属 CSS 伪类
Playwright 扩展了标准 CSS 选择器,增加了实用的伪类:
// 包含文本
page.locator('button:has-text("提交")');
// 匹配文本(精确匹配)
page.locator('button:text("立即提交")');
// 包含子元素
page.locator('.card:has(button.submit)');
// 可见元素
page.locator('.modal:visible');
// 匹配第 N 个
page.locator(':nth-match(".item", 3)');
XPath 定位器
当 CSS 选择器无法精确定位时,XPath 是强大的补充方案:
// 绝对路径
page.locator('//html/body/div[1]/form/input[2]');
// 相对路径
page.locator('//input[@id="username"]');
// 按文本匹配
page.locator('//button[text()="提交"]');
page.locator('//span[contains(text(), "提示")]');
// 按属性匹配
page.locator('//input[@type="email"]');
page.locator('//div[@class="container"]');
page.locator('//*[@data-testid="submit-btn"]');
// 逻辑组合
page.locator('//input[@type="text" and @name="username"]');
page.locator('//button[@type="submit" or @class="btn"]');
// 轴定位
page.locator('//label[@for="email"]/following-sibling::input');
page.locator('//div[@id="main"]/child::p');
page.locator('//li[1]/following-sibling::li[2]');
// 使用文本内容定位
page.locator('//*[contains(text(), "注册")]');
page.locator('//button[normalize-space()="立即注册"]');
XPath vs CSS 选择器
| 场景 | CSS 选择器 | XPath |
|---|---|---|
| ID 定位 | #username | //*[@id="username"] |
| 类名定位 | .class-name | //*[@class="class-name"] |
| 属性定位 | [type="text"] | //*[@type="text"] |
| 文本定位 | 有限支持 | 强大 |
| 层级关系 | 好 | 非常好(支持轴) |
| 性能 | 快 | 稍慢 |
| 可读性 | 简洁 | 冗余 |
文本定位器
文本定位器(Text Locator)通过元素的文本内容来定位,非常适合匹配按钮、链接、标题等包含可见文本的元素:
// 精确文本匹配
page.locator('text=登录');
page.locator('text="立即注册"'); // 引号表示精确匹配
// 模糊文本匹配
page.locator('text=提交'); // 包含"提交"即可
// 在特定元素内匹配文本
page.locator('button', { hasText: '保存' });
page.locator('.menu-item', { hasText: '设置' });
// getByText 方法(推荐)
page.getByText('登录');
page.getByText('用户协议', { exact: true }); // 精确匹配
getByText 的匹配模式
// 默认:模糊匹配(子串匹配)
await page.getByText('确认').click(); // 匹配"确认"、"确认删除"、"确认提交"
// 精确匹配
await page.getByText('确认', { exact: true }).click(); // 仅匹配"确认"
// 正则匹配
await page.getByText(/^确认/).click();
await page.getByText(/提交|保存|确认/).click();
角色定位器
角色定位器(Role Locator)基于 ARIA 角色和可访问性属性定位元素,是构建可访问测试的最佳实践:
// 基本角色定位
page.getByRole('button');
page.getByRole('link');
page.getByRole('heading');
page.getByRole('textbox');
page.getByRole('checkbox');
page.getByRole('radio');
page.getByRole('list');
page.getByRole('listitem');
page.getByRole('navigation');
page.getByRole('dialog');
// 结合名称(推荐)
page.getByRole('button', { name: '提交' });
page.getByRole('link', { name: '了解更多' });
page.getByRole('heading', { name: '用户协议' });
page.getByRole('textbox', { name: '用户名' });
// 常用选项
page.getByRole('button', { pressed: true }); // 按钮被按下状态
page.getByRole('checkbox', { checked: true }); // 复选框已选中
page.getByRole('option', { selected: true }); // 选项被选中
page.getByRole('listitem', { level: 2 }); // 嵌套层级
// 使用正则匹配名称
page.getByRole('button', { name: /确认|提交/ });
角色定位器的优势
- 接近用户视角:按用户看到的交互元素定位,而非 DOM 结构
- 增强可访问性:鼓励使用语义化 HTML 和 ARIA 属性
- 更加稳健:不依赖 CSS 类名或 ID,重构友好
- 内置严格性:自动检查角色是否唯一
常见角色映射
| HTML 元素 | 默认 ARIA 角色 |
|---|---|
<button> | button |
<a href="..."> | link |
<input type="text"> | textbox |
<input type="checkbox"> | checkbox |
<input type="radio"> | radio |
<select> | combobox |
<h1>~<h6> | heading |
<nav> | navigation |
<dialog> | dialog |
<img> | img |
测试ID定位器
测试 ID 定位器通过自定义的 data-testid 属性来定位元素,是 UI 测试中解耦测试代码与样式实现的推荐做法:
// HTML 结构
// <button data-testid="submit-btn">提交</button>
// <input data-testid="email-input" type="email" />
// getByTestId 定位
await page.getByTestId('submit-btn').click();
await page.getByTestId('email-input').fill('[email protected]');
// 自定义测试 ID 属性名
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-cy', // 改为 data-cy 属性
},
});
// 代码中使用
await page.getByTestId('login-form').fill('...'); // 匹配 data-cy="login-form"
测试 ID 最佳实践
// 推荐:语义化的 ID 名称
page.getByTestId('login-submit-button');
page.getByTestId('product-list');
page.getByTestId('search-input');
// 避免:与样式或实现耦合
page.getByTestId('red-button'); // 颜色变化就失效
page.getByTestId('third-item'); // 顺序变化就失效
page.getByTestId('vue-component'); // 技术栈变化就失效
// HTML 中的使用示例
// <form data-testid="login-form">
// <input data-testid="username-input" />
// <input data-testid="password-input" type="password" />
// <button data-testid="login-btn">登录</button>
// </form>
链式定位器
链式定位器允许在一个定位器的基础上进一步缩小范围,实现精准定位:
// 链式调用 locator
const form = page.locator('.login-form');
const submitBtn = form.locator('.submit-btn');
await submitBtn.click();
// 一步完成链式定位
await page.locator('.login-form').locator('.submit-btn').click();
// 多层链式
await page
.locator('nav.main-nav')
.locator('ul.nav-list')
.locator('li.active')
.locator('a')
.click();
// 使用 getBy 方法链式
await page
.getByRole('form', { name: '登录' })
.getByRole('textbox', { name: '用户名' })
.fill('testuser');
// 混合定位器类型
await page
.locator('.product-card')
.filter({ hasText: '限时优惠' })
.getByRole('button', { name: '加入购物车' })
.click();
filter 方法
filter 方法用于在已有的定位器基础上进行过滤,非常适合处理列表或表格中的元素:
// 按文本过滤
const items = page.locator('.list-item');
// 过滤出包含特定文本的项
await items.filter({ hasText: '重要' }).click();
// 过滤出包含特定子元素的项
await items.filter({
has: page.locator('.badge-new'),
}).click();
// 组合过滤条件
await items.filter({
hasText: '进行中',
has: page.locator('.priority-high'),
}).click();
// 表格行过滤
const table = page.locator('table');
const row = table.locator('tr').filter({
has: page.locator('td', { hasText: '张三' }),
});
await row.locator('td:last-child button').click();
// 精确文本过滤
await items.filter({ hasText: '待处理' }).first().click();
await items.filter({ hasText: /已完成|已关闭/ }).click();
filter 的常见应用场景
// 场景一:列表中的特定项
const todoItems = page.locator('.todo-item');
await todoItems.filter({ hasText: '完成报告' }).locator('.checkbox').click();
// 场景二:具有特定状态的项目
await todoItems
.filter({ has: page.locator('.status.done') })
.locator('.delete-btn')
.click();
// 场景三:组合定位器
const productGrid = page.locator('.product-grid');
const discountedProduct = productGrid
.locator('.product-card')
.filter({ hasText: '折扣' })
.filter({ has: page.locator('.tag-hot') });
await discountedProduct.locator('.buy-btn').click();
定位器的自动等待
Playwright 定位器最强大的特性之一就是自动等待机制。在执行操作前,Playwright 会自动等待元素满足操作条件:
// 点击操作前会自动等待元素满足以下条件:
// 1. 元素附加到 DOM
// 2. 元素可见
// 3. 元素稳定(无动画、过渡)
// 4. 元素接收事件(不被遮挡)
// 5. 元素启用(非 disabled)
await page.locator('.submit-btn').click();
// 自定义等待条件
await page.locator('.dynamic-content').waitFor({
state: 'attached', // 附加到 DOM
// state: 'visible' // 可见(默认)
// state: 'hidden' // 隐藏
// state: 'detached' // 从 DOM 移除
timeout: 10000, // 超时时间
});
// 等待特定状态后操作
await page.locator('.loading-spinner').waitFor({ state: 'hidden' });
await page.locator('.loaded-content').waitFor({ state: 'visible' });
总结
Playwright 的定位器系统提供了从基础 CSS 选择器到高级角色定位器的完整解决方案。在实际项目中,建议遵循以下优先级选择定位策略:
- 角色定位器(getByRole):首选方案,基于可访问性语义,最稳健
- 测试 ID 定位器(getByTestId):适合组件测试,与样式和结构解耦
- 文本定位器(getByText):适合定位包含文本的元素
- CSS 选择器:适合复杂结构定位,性能最佳
- XPath:最后选择,用于 CSS 无法完成的复杂定位
通过组合使用链式定位和 filter 方法,配合自动等待机制,Playwright 的定位器能够应对各种复杂的页面结构和动态渲染场景,是构建稳定可靠自动化测试的基石。