Python Playwright 常用元素定位解析
📖 前言
在 UI 自动化测试中,元素定位是基础也是核心。Playwright 提供了多种定位器(Locator)方法,比 Selenium 更强大、更稳定、更符合人体工程学设计。本文详细介绍 Playwright 的各种定位方法,并通过实际示例帮助您掌握高效定位技巧。
🔍 一、Playwright 定位器概述
1.1 定位器 vs 选择器
Playwright 使用定位器(Locator)代替传统的选择器(Selector),具有以下优势:
| 特性 | 选择器(Selenium) | 定位器(Playwright) |
|---|---|---|
| 自动等待 | 需手动等待 | 默认自动等待 |
| 重试机制 | 无 | 内置智能重试 |
| 可见性检查 | 无 | 自动过滤不可见元素 |
| 稳定性 | 依赖性强 | 更稳定可靠 |
| 可读性 | 较差 | 语义化更强 |
1.2 定位器类型总览
定位器类型
├── get_by_role() # 角色定位(最推荐)
├── get_by_text() # 文本定位
├── get_by_label() # 标签定位
├── get_by_placeholder() # 占位符定位
├── get_by_test_id() # 测试ID定位
├── locator() # CSS/XPath定位
└── page.locator() # 组合定位
🎯 二、推荐定位方法(按效率排序)
2.1 get_by_role() - 角色定位 ⭐⭐⭐⭐⭐
推荐指数:⭐⭐⭐⭐⭐
原因:
-
最语义化,符合 HTML 语义
-
不依赖视觉样式,不易受 CSS 影响
-
对前端重构友好
-
Playwright 官方首推
使用示例:
from playwright.sync_api import Page
def test_role_locator(page: Page):
"""角色定位示例"""
# 1. 按钮
page.get_by_role("button", name="提交").click()
page.get_by_role("button", name=re.compile("提交|确认")).click()
# 2. 链接
page.get_by_role("link", name="查看详情").click()
# 3. 输入框
page.get_by_role("textbox", name="用户名").fill("admin")
page.get_by_role("textbox", name="密码").fill("123456")
# 4. 复选框
page.get_by_role("checkbox", name="记住我").check()
# 5. 单选框
page.get_by_role("radio", name="男").check()
# 6. 下拉列表
page.get_by_role("combobox", name="选择城市").select_option("北京")
# 7. 表格
page.get_by_role("row", name="第一行").click()
# 8. 列表项
page.get_by_role("listitem").first.click()
# 9. 导航菜单
page.get_by_role("menuitem", name="个人中心").click()
# 10. 警告框
page.get_by_role("alert").click()
常用角色列表:
| 角色 | HTML元素 | 用途 |
|---|---|---|
|
|
| 按钮 |
|
|
| 链接 |
|
|
| 输入框 |
|
|
| 复选框 |
|
|
| 单选框 |
|
|
| 下拉列表 |
|
|
| 列表 |
|
|
| 列表项 |
|
| 菜单项 | 菜单 |
|
|
| 菜单容器 |
|
|
| 表格行 |
|
|
| 表格单元格 |
|
|
| 表格 |
|
|
| 对话框 |
|
| 警告元素 | 警告 |
|
|
| 图片 |
|
|
| 标题 |
|
|
| 段落 |
2.2 get_by_text() - 文本定位 ⭐⭐⭐⭐
推荐指数:⭐⭐⭐⭐
特点:
-
基于元素文本内容定位
-
支持精确匹配和模糊匹配
-
对国际化友好
使用示例:
def test_text_locator(page: Page):
"""文本定位示例"""
# 1. 精确匹配
page.get_by_text("提交").click()
page.get_by_text("用户名").fill("admin")
# 2. 包含文本(模糊匹配)
page.get_by_text("提交", exact=False).click()
# 3. 正则表达式
import re
page.get_by_text(re.compile(r"用户\d+")).click()
# 4. 多条件
page.get_by_text("确定", exact=True).click()
# 5. 定位段落
page.get_by_text("这是一段说明文字").is_visible()
# 6. 定位标题
h1 = page.get_by_role("heading").filter(has_text="欢迎")
2.3 get_by_label() - 标签定位 ⭐⭐⭐⭐
推荐指数:⭐⭐⭐⭐
适用场景:
-
表单输入框
-
有明确标签的控件
使用示例:
def test_label_locator(page: Page):
"""标签定位示例"""
# 1. 基础使用
page.get_by_label("用户名").fill("admin")
page.get_by_label("密码").fill("123456")
# 2. 占位符作为标签
page.get_by_label("请输入手机号").fill("13800138000")
# 3. aria-label
page.get_by_label("搜索").fill("关键词")
# 4. 关联的label标签
# <label for="username">用户名</label>
# <input id="username" />
page.get_by_label("用户名").fill("admin")
# 5. 区分大小写
page.get_by_label("Email", exact=True).fill("test@example.com")
2.4 get_by_placeholder() - 占位符定位 ⭐⭐⭐
推荐指数:⭐⭐⭐
适用场景:
-
搜索框
-
输入提示
使用示例:
def test_placeholder_locator(page: Page):
"""占位符定位示例"""
# 1. 精确匹配
page.get_by_placeholder("请输入用户名").fill("admin")
# 2. 模糊匹配
page.get_by_placeholder("搜索", exact=False).fill("关键词")
# 3. 常见场景
page.get_by_placeholder("搜索...").click()
page.get_by_placeholder("输入邮箱").fill("test@example.com")
page.get_by_placeholder("输入密码").fill("password")
2.5 get_by_test_id() - 测试ID定位 ⭐⭐⭐⭐⭐
推荐指数:⭐⭐⭐⭐⭐(最稳定)
前提条件:
-
前端需要添加
data-testid属性
使用示例:
<!-- 前端代码 -->
<button data-testid="submit-btn">提交</button>
<input data-testid="username-input" />
<div data-testid="error-message"></div>
def test_testid_locator(page: Page):
"""测试ID定位示例"""
# 1. 基础使用
page.get_by_test_id("submit-btn").click()
page.get_by_test_id("username-input").fill("admin")
# 2. 获取文本
error_msg = page.get_by_test_id("error-message").text_content()
# 3. 验证状态
assert page.get_by_test_id("submit-btn").is_enabled()
# 4. 设置测试ID
# Playwright会自动查找 data-testid 或 testId
page.get_by_test_id("submit-button").click()
优势:
# 对比:传统方式 vs 测试ID
# ❌ 传统方式(脆弱)
page.locator(".btn.primary.submit-btn").click() # CSS变化就失效
# ✅ 测试ID(稳定)
page.get_by_test_id("submit-btn").click() # 独立于样式
🔧 三、CSS 和 XPath 定位
3.1 CSS 定位 ⭐⭐⭐
使用场景:
-
复杂选择器
-
组选择器
示例:
def test_css_locator(page: Page):
"""CSS定位示例"""
# 1. ID选择器
page.locator("#username").fill("admin")
# 2. 类选择器
page.locator(".btn-primary").click()
page.locator(".form-control").first.fill("value")
# 3. 属性选择器
page.locator("[type='submit']").click()
page.locator("[data-id='submit']").click()
page.locator("[placeholder='用户名']").fill("admin")
# 4. 组合选择器
page.locator("div.modal .btn-primary").click()
page.locator("form.login .submit-btn").click()
# 5. 伪类选择器
page.locator("input:not([disabled])").first.fill("value") # 未禁用的输入框
page.locator("button:first-of-type").click() # 第一个按钮
# 6. nth选择器
page.locator(".item").nth(0).click() # 第一个
page.locator(".item").nth(-1).click() # 最后一个
# 7. 兄弟选择器
page.locator("label + input").fill("value") # label后的第一个input
page.locator("label ~ input").fill("value") # label后的所有input
3.2 XPath 定位 ⭐⭐
使用场景:
-
父元素到子元素
-
复杂路径关系
示例:
def test_xpath_locator(page: Page):
"""XPath定位示例"""
# 1. 绝对路径(不推荐)
page.locator("xpath=/html/body/div[2]/form/input[1]").fill("admin")
# 2. 相对路径
page.locator("xpath=//input[@id='username']").fill("admin")
# 3. 文本内容
page.locator("xpath=//button[text()='提交']").click()
page.locator("xpath=//span[contains(text(),'用户名')]").is_visible()
# 4. 属性匹配
page.locator("xpath=//input[@type='submit']").click()
page.locator("xpath=//div[@data-testid='container']").click()
# 5. 层级关系
page.locator("xpath=//form//button").click() # form下的任意后代button
page.locator("xpath=//div[@class='header']/button").click() # header的直接子元素
# 6. 逻辑运算
page.locator("xpath=//input[@type='text' and @name='username']").fill("admin")
page.locator("xpath=//button[@id='btn' or @class='submit']").click()
# 7. 轴定位
page.locator("xpath=//label[contains(text(),'用户名')]/following-sibling::input").fill("admin")
🎪 四、组合定位与高级技巧
4.1 过滤器 filter()
def test_filter(page: Page):
"""过滤器使用示例"""
# 1. 按文本过滤
page.get_by_role("listitem").filter(has_text="第一项").click()
# 2. 按多个条件过滤
page.locator(".product-card").filter(
has=page.get_by_text("iPhone")
).filter(
has=page.locator(".price").filter(has_text="5999")
).click()
# 3. has_not 排除
page.locator(".user-item").filter(
has_not=page.get_by_text("已删除")
).first.click()
# 4. 链式过滤
items = page.locator(".todo-item").filter(has_text="完成")
for item in items.all():
print(item.text_content())
4.2 链式定位
def test_chained_locator(page: Page):
"""链式定位示例"""
# 1. 从容器开始
page.locator(".sidebar").get_by_text("设置").click()
# 2. 父级链
page.locator(".menu-item").last.locator(".submenu-item").first.click()
# 3. 嵌套查找
page.locator(".card").locator(".card-header").get_by_text("标题").click()
page.locator(".card").locator(".card-body").locator("button").click()
# 4. 组合多种定位
page.locator(".form") \
.get_by_label("用户名") \
.fill("admin")
# 5. 查找子元素
card = page.locator(".card").filter(has_text="卡片标题")
card.locator(".btn").click()
4.3 智能等待
def test_smart_wait(page: Page):
"""智能等待示例"""
# 1. 自动等待(默认开启)
page.get_by_role("button", name="提交").click() # Playwright自动等待元素可点击
# 2. 显式等待条件
page.get_by_text("加载完成").wait_for(timeout=5000)
# 3. 等待元素出现
page.locator(".modal").wait_for(state="attached")
# 4. 等待元素消失
page.locator(".loading").wait_for(state="detached")
# 5. 等待元素可见
page.locator(".result").wait_for(state="visible")
# 6. 等待元素隐藏
page.locator(".spinner").wait_for(state="hidden")
# 7. 自定义等待
page.locator(".data-loaded").wait_for(
lambda locator: "100%" in locator.text_content(),
timeout=10000
)
📊 五、定位效率对比
5.1 效率排名
| 排名 | 定位方法 | 速度 | 稳定性 | 维护成本 |
|---|---|---|---|---|
| 🥇 1 |
| 极快 | ⭐⭐⭐⭐⭐ | ⭐ |
| 🥈 2 |
| 快 | ⭐⭐⭐⭐⭐ | ⭐ |
| 🥉 3 |
| 快 | ⭐⭐⭐⭐ | ⭐ |
| 4 |
| 快 | ⭐⭐⭐ | ⭐⭐ |
| 5 |
| 快 | ⭐⭐⭐ | ⭐ |
| 6 | CSS 选择器 | 快 | ⭐⭐ | ⭐⭐⭐ |
| 7 | XPath | 较慢 | ⭐ | ⭐⭐⭐⭐ |
5.2 最佳实践建议
┌─────────────────────────────────────────────────────┐
│ 定位优先级 │
├─────────────────────────────────────────────────────┤
│ 1. ✅ get_by_test_id() ← 首选,最稳定 │
│ 2. ✅ get_by_role() ← 语义化,官方推荐 │
│ 3. ✅ get_by_label() ← 表单元素首选 │
│ 4. ⚠️ get_by_text() ← 需要精确匹配时使用 │
│ 5. ⚠️ CSS 选择器 ← 组合条件时使用 │
│ 6. ❌ XPath ← 尽量避免使用 │
└─────────────────────────────────────────────────────┘
5.3 代码示例:最佳实践
# ❌ 不推荐:脆弱的CSS选择器
page.locator("#main > div.content > form > div:nth-child(2) > input").fill("admin")
# ❌ 不推荐:XPath
page.locator("xpath=//div[@class='content']//form//input[@name='username']").fill("admin")
# ❌ 不推荐:依赖样式
page.locator("button.btn.btn-primary.submit-btn").click()
# ✅ 推荐:语义化定位
page.get_by_label("用户名").fill("admin")
page.get_by_role("button", name="提交").click()
# ✅ 推荐:添加测试ID
# <input data-testid="username-input" />
page.get_by_test_id("username-input").fill("admin")
# ✅ 推荐:组合定位
page.get_by_role("form").get_by_label("用户名").fill("admin")
🎯 六、实战案例
6.1 登录表单
def test_login_form(page: Page):
"""登录表单测试"""
# 方法1:标签 + 占位符(推荐)
page.get_by_label("用户名").fill("admin")
page.get_by_label("密码").fill("password123")
page.get_by_role("checkbox", name="记住我").check()
page.get_by_role("button", name="登录").click()
# 方法2:测试ID(最稳定)
page.get_by_test_id("username-input").fill("admin")
page.get_by_test_id("password-input").fill("password123")
page.get_by_test_id("remember-checkbox").check()
page.get_by_test_id("login-button").click()
# 验证
page.get_by_text("登录成功").wait_for(timeout=5000)
6.2 表格操作
def test_table_operation(page: Page):
"""表格操作测试"""
# 1. 定位表格
table = page.locator("table.user-table")
# 2. 查找特定行
target_row = table.get_by_role("row").filter(has_text="张三")
# 3. 行内操作
target_row.get_by_role("button", name="编辑").click()
# 4. 复杂筛选
rows = table.get_by_role("row").filter(has=page.locator("td.status").filter(has_text="启用"))
for row in rows.all():
name = row.locator("td.name").text_content()
print(f"启用的用户: {name}")
# 5. 分页操作
page.get_by_role("button", name="下一页").click()
page.locator(".pagination .active").wait_for_text("2")
6.3 下拉菜单
def test_dropdown(page: Page):
"""下拉菜单测试"""
# 方法1:角色定位(推荐)
page.get_by_role("combobox", name="选择城市").click()
page.get_by_role("option", name="北京").click()
# 方法2:选择器
page.locator("select#city").select_option("beijing")
# 方法3:输入搜索
page.locator(".city-select input").fill("北京")
page.locator(".city-select option:has-text('北京')").click()
6.4 弹框处理
def test_dialogs(page: Page):
"""弹框处理测试"""
# 1. 确认对话框
page.get_by_role("button", name="删除").click()
page.get_by_role("button", name="确定").click()
# 2. 警告框
alert = page.get_by_role("alert")
alert.get_by_role("button", name="关闭").click()
# 3. 模态框
page.locator(".modal").wait_for(state="visible")
page.locator(".modal .close-btn").click()
page.locator(".modal").wait_for(state="hidden")
# 4. Toast提示
toast = page.locator(".toast")
toast.wait_for(state="visible")
assert "操作成功" in toast.text_content()
toast.wait_for(state="hidden")
🔧 七、调试技巧
7.1 快速定位元素
# 在浏览器控制台执行,查看元素选择器
playwright.selector("点击的元素")
# 或者使用Pick工具
page.pick_locator() # 交互式选择器生成
7.2 调试脚本
def test_debug(page: Page):
"""调试技巧"""
# 1. 开启跟踪
page.context.tracing.start(screenshots=True, snapshots=True)
# ... 执行操作
page.context.tracing.stop(path="trace.zip")
# 2. 截图保存
page.screenshot(path="debug.png")
# 3. 打印HTML
print(page.locator(".target").inner_html())
# 4. 查看元素信息
print(page.locator(".target").count()) # 数量
print(page.locator(".target").all_inner_texts()) # 所有文本
# 5. 记录日志
from playwright.sync_api import Page
def log_request(request):
print(f"Request: {request.url}")
page.on("request", log_request)
7.3 常见问题排查
def test_troubleshooting(page: Page):
"""问题排查技巧"""
# 问题1:找不到元素
# 解决方案:检查元素是否在iframe中
frame = page.frame_locator("iframe[name='content']")
frame.get_by_text("内容").click()
# 问题2:元素被遮挡
# 解决方案:滚动到视图
page.get_by_text("底部按钮").scroll_into_view_if_needed()
page.get_by_text("底部按钮").click(force=True) # 强制点击
# 问题3:元素不可见
# 解决方案:等待或使用force
page.get_by_text("动态加载").wait_for(state="attached")
page.get_by_text("隐藏元素").click(force=True)
# 问题4:多个匹配元素
# 解决方案:精确筛选
page.get_by_role("button", name="删除").filter(has_text="用户").first.click()
📝 八、总结
8.1 定位方法速查表
| 场景 | 推荐方法 | 示例 |
|---|---|---|
| 表单输入 |
|
|
| 按钮点击 |
|
|
| 链接跳转 |
|
|
| 搜索框 |
|
|
| 固定元素 |
|
|
| 动态文本 |
|
|
| 复杂选择 | CSS + filter |
|
8.2 一句话建议
优先使用
get_by_test_id()和get_by_role(),它们是 Playwright 中最稳定、最语义化的定位方式。
📚 资源链接
| 资源 | 链接 |
|---|---|
| Playwright Locators | |
| Playwright Selectors | |
| Best Practices |
如果觉得有帮助,欢迎点赞、收藏!
关注作者,获取更多测试开发干货~
更多推荐



所有评论(0)