Playwright 元素定位 #
Locator 概述 #
Locator 是 Playwright 的核心概念,它代表了一种查找页面元素的方式。Locator 具有自动等待和重试机制,确保操作可靠执行。
Locator 的特点 #
text
┌─────────────────────────────────────────────────────────────┐
│ Locator 特点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 自动等待 - 等待元素可见、可点击 │
│ ✅ 自动重试 - 断言失败时自动重试 │
│ ✅ 严格模式 - 匹配多个元素时报错 │
│ ✅ 链式调用 - 支持组合和过滤 │
│ ✅ 弹性定位 - DOM 变化后仍能找到元素 │
│ │
└─────────────────────────────────────────────────────────────┘
创建 Locator #
typescript
import { test, expect } from '@playwright/test';
test('创建 Locator', async ({ page }) => {
// 基本方式
const button = page.locator('button');
// 推荐方式(语义化)
const submitBtn = page.getByRole('button', { name: 'Submit' });
// 使用 Locator
await submitBtn.click();
});
推荐的定位方式 #
优先级排序 #
text
推荐优先级(从高到低):
1. getByRole - 基于可访问性角色(最推荐)
2. getByText - 基于文本内容
3. getByLabel - 基于标签文本
4. getByPlaceholder - 基于 placeholder
5. getByAltText - 基于 alt 属性
6. getByTitle - 基于 title 属性
7. getByTestId - 基于 data-testid
8. CSS 选择器 - 最后选择
getByRole - 角色选择器 #
基本用法 #
typescript
import { test, expect } from '@playwright/test';
test('getByRole 示例', async ({ page }) => {
// 按钮
await page.getByRole('button').click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button', { name: /submit/i }).click();
// 链接
await page.getByRole('link', { name: 'Home' }).click();
// 标题
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Main Title');
// 导航
await page.getByRole('navigation').getByRole('link', { name: 'Products' }).click();
// 列表
const items = page.getByRole('listitem');
await expect(items).toHaveCount(5);
// 复选框
await page.getByRole('checkbox', { name: 'Accept terms' }).check();
// 单选按钮
await page.getByRole('radio', { name: 'Option 1' }).check();
// 文本框
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
// 组合框
await page.getByRole('combobox', { name: 'Country' }).selectOption('CN');
});
常用角色列表 #
| 角色 | HTML 元素 | 说明 |
|---|---|---|
button |
<button>, <input type="button"> |
按钮 |
link |
<a> |
链接 |
heading |
<h1> - <h6> |
标题 |
textbox |
<input type="text">, <textarea> |
文本输入框 |
checkbox |
<input type="checkbox"> |
复选框 |
radio |
<input type="radio"> |
单选按钮 |
combobox |
<select> |
下拉框 |
listitem |
<li> |
列表项 |
navigation |
<nav> |
导航 |
main |
<main> |
主内容区 |
article |
<article> |
文章 |
dialog |
<dialog> |
对话框 |
alert |
[role="alert"] |
警告提示 |
tab |
[role="tab"] |
标签页 |
角色选择器选项 #
typescript
// name - 精确匹配
await page.getByRole('button', { name: 'Submit' });
// name - 正则匹配
await page.getByRole('button', { name: /submit/i });
// exact - 精确匹配
await page.getByRole('button', { name: 'Submit', exact: true });
// level - 标题级别
await page.getByRole('heading', { level: 1 });
// checked - 选中状态
await page.getByRole('checkbox', { checked: true });
// disabled - 禁用状态
await page.getByRole('button', { disabled: true });
// expanded - 展开状态
await page.getByRole('button', { expanded: true });
// selected - 选中状态
await page.getByRole('tab', { selected: true });
getByText - 文本选择器 #
基本用法 #
typescript
test('getByText 示例', async ({ page }) => {
// 精确匹配
await page.getByText('Welcome').click();
// 正则匹配
await page.getByText(/welcome/i).click();
// 部分匹配
await page.getByText('Wel', { exact: false }).click();
// 组合使用
await page.locator('.card').getByText('Learn More').click();
});
文本选择器选项 #
typescript
// 精确匹配(默认 false)
await page.getByText('Submit', { exact: true });
// 正则表达式
await page.getByText(/submit/i);
await page.getByText(/^Submit$/);
await page.getByText(/Price: \$\d+/);
getByLabel - 标签选择器 #
基本用法 #
typescript
test('getByLabel 示例', async ({ page }) => {
// 通过 label 文本查找表单元素
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
// 正则匹配
await page.getByLabel(/email/i).fill('test@example.com');
// 精确匹配
await page.getByLabel('Email Address', { exact: true }).fill('test@example.com');
});
对应的 HTML 结构 #
html
<!-- 方式 1: label 包裹 -->
<label>
Email
<input type="email" />
</label>
<!-- 方式 2: for 属性关联 -->
<label for="email-input">Email</label>
<input id="email-input" type="email" />
<!-- 方式 3: aria-labelledby -->
<input aria-labelledby="email-label" type="email" />
<span id="email-label">Email</span>
getByPlaceholder - 占位符选择器 #
基本用法 #
typescript
test('getByPlaceholder 示例', async ({ page }) => {
// 精确匹配
await page.getByPlaceholder('Enter your email').fill('test@example.com');
// 正则匹配
await page.getByPlaceholder(/email/i).fill('test@example.com');
// 组合使用
await page.locator('.login-form').getByPlaceholder('Password').fill('password');
});
对应的 HTML 结构 #
html
<input placeholder="Enter your email" />
<input placeholder="Enter your password" />
<textarea placeholder="Write your message..."></textarea>
getByAltText - Alt 文本选择器 #
基本用法 #
typescript
test('getByAltText 示例', async ({ page }) => {
// 图片
await expect(page.getByAltText('Company Logo')).toBeVisible();
// 正则匹配
await expect(page.getByAltText(/logo/i)).toBeVisible();
});
对应的 HTML 结构 #
html
<img src="logo.png" alt="Company Logo" />
<area alt="Map region" />
<input type="image" alt="Submit form" />
getByTitle - 标题属性选择器 #
基本用法 #
typescript
test('getByTitle 示例', async ({ page }) => {
// 精确匹配
await page.getByTitle('Close dialog').click();
// 正则匹配
await page.getByTitle(/close/i).click();
});
对应的 HTML 结构 #
html
<button title="Close dialog">X</button>
<span title="More information">?</span>
getByTestId - 测试 ID 选择器 #
基本用法 #
typescript
test('getByTestId 示例', async ({ page }) => {
// 基本用法
await page.getByTestId('submit-button').click();
// 正则匹配
await page.getByTestId(/^submit-/).click();
// 组合使用
await page.locator('.form').getByTestId('email-input').fill('test@example.com');
});
自定义测试 ID 属性 #
typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-cy', // 自定义属性名
},
});
html
<!-- 默认 data-testid -->
<button data-testid="submit-button">Submit</button>
<!-- 自定义 data-cy -->
<button data-cy="submit-button">Submit</button>
CSS 选择器 #
基本用法 #
typescript
test('CSS 选择器示例', async ({ page }) => {
// ID 选择器
await page.locator('#submit').click();
// 类选择器
await page.locator('.btn-primary').click();
// 标签选择器
await page.locator('button').click();
// 属性选择器
await page.locator('[data-id="123"]').click();
await page.locator('[type="submit"]').click();
// 组合选择器
await page.locator('button.btn-primary').click();
await page.locator('form .submit-btn').click();
// 后代选择器
await page.locator('nav ul li a').click();
// 子选择器
await page.locator('ul > li').first().click();
// 兄弟选择器
await page.locator('h1 + p').click();
});
CSS 伪类 #
typescript
test('CSS 伪类示例', async ({ page }) => {
// :visible - 可见元素
await page.locator('button:visible').click();
// :nth-child - 第 n 个子元素
await page.locator('li:nth-child(2)').click();
// :first-child - 第一个子元素
await page.locator('li:first-child').click();
// :last-child - 最后一个子元素
await page.locator('li:last-child').click();
// :has - 包含特定元素
await page.locator('article:has(h1)').click();
// :is - 匹配任意选择器
await page.locator(':is(button, a).primary').click();
});
XPath 选择器 #
基本用法 #
typescript
test('XPath 示例', async ({ page }) => {
// 使用 xpath= 前缀
await page.locator('xpath=//button[@type="submit"]').click();
// 或使用 // 开头
await page.locator('//button[@type="submit"]').click();
// 文本匹配
await page.locator('//button[contains(text(), "Submit")]').click();
// 属性匹配
await page.locator('//*[@data-testid="submit-btn"]').click();
});
Locator 链式调用 #
组合定位器 #
typescript
test('链式调用示例', async ({ page }) => {
// 在特定区域内查找
const form = page.locator('.login-form');
await form.getByLabel('Email').fill('test@example.com');
await form.getByLabel('Password').fill('password');
await form.getByRole('button', { name: 'Sign In' }).click();
// 链式调用
await page
.locator('.product-list')
.locator('.product')
.filter({ hasText: 'iPhone' })
.getByRole('button', { name: 'Add to Cart' })
.click();
});
过滤 Locator #
typescript
test('过滤示例', async ({ page }) => {
// hasText - 包含文本
await page.locator('.item').filter({ hasText: 'Sale' }).click();
// hasNotText - 不包含文本
await page.locator('.item').filter({ hasNotText: 'Out of Stock' }).click();
// has - 包含子元素
await page.locator('.product').filter({
has: page.getByRole('button', { name: 'Buy' })
}).click();
// hasNot - 不包含子元素
await page.locator('.product').filter({
hasNot: page.getByText('Sold Out')
}).click();
});
Locator 操作方法 #
获取元素数量 #
typescript
test('获取数量', async ({ page }) => {
const items = page.getByRole('listitem');
const count = await items.count();
console.log(`共有 ${count} 个列表项`);
await expect(items).toHaveCount(5);
});
遍历元素 #
typescript
test('遍历元素', async ({ page }) => {
const items = page.getByRole('listitem');
const count = await items.count();
for (let i = 0; i < count; i++) {
const text = await items.nth(i).textContent();
console.log(`Item ${i}: ${text}`);
}
// 或使用 all()
const allItems = await items.all();
for (const item of allItems) {
const text = await item.textContent();
console.log(text);
}
});
获取特定元素 #
typescript
test('获取特定元素', async ({ page }) => {
const items = page.getByRole('listitem');
// 第一个
await items.first().click();
// 最后一个
await items.last().click();
// 第 n 个(从 0 开始)
await items.nth(2).click();
});
获取元素信息 #
typescript
test('获取元素信息', async ({ page }) => {
const button = page.getByRole('button', { name: 'Submit' });
// 获取文本内容
const text = await button.textContent();
// 获取内部文本
const innerText = await button.innerText();
// 获取属性值
const disabled = await button.getAttribute('disabled');
// 获取输入值
const input = page.getByRole('textbox');
const value = await input.inputValue();
// 检查是否可见
const isVisible = await button.isVisible();
// 检查是否启用
const isEnabled = await button.isEnabled();
// 检查是否选中
const checkbox = page.getByRole('checkbox');
const isChecked = await checkbox.isChecked();
});
最佳实践 #
1. 优先使用语义化选择器 #
typescript
// ✅ 推荐 - 语义化
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('test@example.com');
// ❌ 不推荐 - 耦合样式
await page.locator('.btn.btn-primary.submit').click();
await page.locator('#email-input').fill('test@example.com');
2. 使用链式调用缩小范围 #
typescript
// ✅ 推荐 - 明确范围
await page.locator('.login-form')
.getByRole('button', { name: 'Submit' })
.click();
// ❌ 不推荐 - 范围太大
await page.getByRole('button', { name: 'Submit' }).click();
3. 使用正则表达式增加灵活性 #
typescript
// ✅ 推荐 - 灵活匹配
await page.getByRole('button', { name: /submit/i }).click();
await page.getByText(/price: \$\d+/).click();
// ❌ 不推荐 - 过于严格
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Price: $99').click();
4. 使用 data-testid 作为后备 #
typescript
// 当没有合适的语义化选择器时
await page.getByTestId('product-card-123').click();
5. 避免使用 XPath #
typescript
// ✅ 推荐
await page.getByRole('button', { name: 'Submit' }).click();
// ❌ 不推荐
await page.locator('//div[@class="form"]/button[@type="submit"]').click();
下一步 #
现在你已经掌握了 Playwright 元素定位的方法,接下来学习 页面操作 了解如何与页面元素交互!
最后更新:2026-03-28