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