Playwright 第16章:定位器(Locators)

定位器(Locators)是 Playwright 自动化测试的核心概念。它是查找和操作页面元素的统一 API,相比传统的 DOM 操作,定位器具有自动等待、重试机制和更好的可读性等优势。本章将全面介绍 Playwright 的各种定位策略。

定位器概念

什么是定位器

定位器是 Playwright 中用于在页面上查找元素的核心抽象。与传统的 elementHandledocument.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 选择器到高级角色定位器的完整解决方案。在实际项目中,建议遵循以下优先级选择定位策略:

  1. 角色定位器(getByRole):首选方案,基于可访问性语义,最稳健
  2. 测试 ID 定位器(getByTestId):适合组件测试,与样式和结构解耦
  3. 文本定位器(getByText):适合定位包含文本的元素
  4. CSS 选择器:适合复杂结构定位,性能最佳
  5. XPath:最后选择,用于 CSS 无法完成的复杂定位

通过组合使用链式定位和 filter 方法,配合自动等待机制,Playwright 的定位器能够应对各种复杂的页面结构和动态渲染场景,是构建稳定可靠自动化测试的基石。