📖 前言

在 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元素

用途

button

<button>, <input type="button">

按钮

link

<a>

链接

textbox

<input>, <textarea>

输入框

checkbox

<input type="checkbox">

复选框

radio

<input type="radio">

单选框

combobox

<select>

下拉列表

listbox

<ul>, <ol>

列表

listitem

<li>

列表项

menuitem

菜单项

菜单

menu

<menu>

菜单容器

row

<tr>

表格行

cell

<td>, <th>

表格单元格

grid

<table>

表格

dialog

<dialog>

对话框

alert

警告元素

警告

img

<img>

图片

heading

<h1>-<h6>

标题

paragraph

<p>

段落

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

get_by_test_id()

极快

⭐⭐⭐⭐⭐

🥈 2

get_by_role()

⭐⭐⭐⭐⭐

🥉 3

get_by_label()

⭐⭐⭐⭐

4

get_by_text()

⭐⭐⭐

⭐⭐

5

get_by_placeholder()

⭐⭐⭐

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 定位方法速查表

场景

推荐方法

示例

表单输入

get_by_label()

page.get_by_label("用户名")

按钮点击

get_by_role()

page.get_by_role("button", name="提交")

链接跳转

get_by_role("link")

page.get_by_role("link", name="详情")

搜索框

get_by_placeholder()

page.get_by_placeholder("搜索...")

固定元素

get_by_test_id()

page.get_by_test_id("submit-btn")

动态文本

get_by_text()

page.get_by_text("欢迎", exact=False)

复杂选择

CSS + filter

page.locator(".card").filter(has_text="标题")

8.2 一句话建议

优先使用 get_by_test_id()get_by_role(),它们是 Playwright 中最稳定、最语义化的定位方式。


📚 资源链接

资源

链接

Playwright Locators

https://playwright.dev/python/docs/locators

Playwright Selectors

https://playwright.dev/python/docs/selectors

Best Practices

https://playwright.dev/python/docs/best-practices


如果觉得有帮助,欢迎点赞、收藏!

关注作者,获取更多测试开发干货~

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐