Playwright元素定位全攻略:XPath/CSS选择器实战技巧(Python版)

最近在帮团队重构自动化测试框架,从Selenium迁移到Playwright的过程中,最让我头疼的不是API的变化,而是如何写出既稳定又易维护的元素定位代码。相信很多刚开始接触Playwright的Python开发者都有类似的经历——明明在浏览器开发者工具里能精准选中的元素,到了脚本里就死活定位不到,或者今天能跑通的脚本,明天页面稍微改个样式就全挂了。

这篇文章就是我在踩了无数坑之后,总结出的一套Playwright元素定位实战心法。我不会重复官方文档里那些基础语法,而是聚焦于如何在实际项目中构建健壮的定位策略,特别是针对动态加载、复杂表单、单页应用这些让自动化测试变得脆弱的场景。无论你是从零开始搭建测试体系,还是正在为定位不稳定而烦恼,这里面的思路和技巧应该都能给你带来直接的帮助。

1. 定位策略的底层逻辑:为什么你的选择器总失效?

在深入具体语法之前,我们先得理解Playwright定位机制的运作原理。很多人把元素定位简单地理解为“写个CSS或XPath表达式”,但实际上,定位的稳定性取决于你对浏览器渲染时机和页面结构的理解深度

Playwright的locator()方法创建的是一个“查找器”,它并不会立即在DOM中搜索元素,而是在你执行操作(如click()fill())时,才实时地去查找匹配的元素。这个机制带来了自动等待的优势,但也意味着如果你的选择器写得不够精准,Playwright可能会等到超时,或者匹配到错误的元素。

1.1 动态内容的挑战与应对

现代Web应用大量使用JavaScript动态生成内容,这给定位带来了两个核心挑战:

  1. 元素尚未加载完成:这是最常见的问题。你写了一个选择器指向某个按钮,但脚本执行时,这个按钮可能还在由某个异步请求渲染中。
  2. DOM结构频繁变动:特别是在React、Vue等框架构建的单页应用中,组件的复用和动态渲染可能导致元素的属性、甚至层级结构发生变化。

面对动态内容,我的第一条原则是:尽量避免使用依赖于绝对位置或易变属性的定位方式。比如下面这个XPath就非常脆弱:

//div[@id='app']/div[2]/div/div[3]/button[1]

这个路径完全依赖于元素在DOM树中的固定位置,只要前端开发在div[2]前面加了一个新的div,你的整个定位链就断了。更糟糕的是,这类错误往往在测试运行时才暴露出来,排查起来非常耗时。

提示:在Playwright中,你可以利用其内置的自动等待机制来应对动态加载。但自动等待的前提是,你的选择器本身是稳定且唯一的。如果选择器本身就能匹配到多个元素,或者匹配逻辑模糊,自动等待也救不了你。

1.2 评估定位器健壮性的维度

如何判断一个定位器是否“健壮”?我通常会从以下几个维度来评估:

  • 唯一性:在当前页面上下文中,是否能且仅能匹配到目标元素?
  • 可读性:其他团队成员(包括非自动化测试人员)能否一眼看懂这个选择器在找什么?
  • 抗变更性:当页面样式微调、增加无关元素或组件结构小幅重构时,这个选择器是否依然有效?
  • 性能:选择器的查询复杂度如何?过于复杂的嵌套选择器可能会影响脚本执行速度。

在实际项目中,我建议为重要的定位器(如核心业务流程中的按钮、输入框)建立简单的“健康检查”机制。例如,在定位器旁边添加注释,说明其设计意图和潜在的脆弱点。

2. CSS选择器:精准与简洁的艺术

CSS选择器是我在Playwright中的首选,因为它通常更简洁、性能更好,并且与前端开发的思维方式高度一致。但要用好CSS选择器,你需要超越基础的idclass选择。

2.1 超越基础:属性选择器的实战技巧

当元素没有唯一的idclass时,属性选择器是你的利器。但直接使用[attribute="value"]有时仍然不够稳定,因为前端框架可能会动态生成属性值的一部分。

考虑这样一个常见的搜索输入框:

<input type="text" data-testid="search-input" placeholder="请输入关键词..." class="ant-input search-box">

最脆弱的定位方式是:

page.locator('.ant-input').fill('keyword')

页面上可能有多个.ant-input类的输入框。

稍好一些,但仍有风险:

page.locator('.search-box').fill('keyword')

如果CSS类名因样式重构被修改,定位就会失效。

我推荐的策略是使用自定义数据属性,并与开发团队约定规范:

page.locator('[data-testid="search-input"]').fill('keyword')

data-testid这类属性是专门为测试而生的,前端开发在修改样式时通常不会动它,稳定性极高。如果项目没有这样的规范,你可以推动团队建立它,这是提升自动化测试稳定性的低成本高回报投资。

对于属性值可能动态变化的情况,可以使用部分匹配运算符:

运算符含义示例适用场景
^=以指定值开头[href^="https://api"]匹配特定前缀的API链接
$=以指定值结尾[src$=".png"]匹配特定后缀的资源
*=包含指定值[class*="btn-"]匹配包含某类名的所有按钮
~=包含完整单词[class~="primary"]匹配类列表中包含独立单词primary

例如,要定位一个动态生成的用户头像,其src属性可能是https://cdn.example.com/avatars/user_12345_v2.jpg,你可以这样写:

avatar = page.locator('img[src*="avatars/user_"]')

这样即使版本号(_v2)变化,定位器依然有效。

2.2 处理复杂层级与状态

对于嵌套较深的元素,CSS选择器可以通过组合来精确命中。但要注意避免过度嵌套,一般建议不超过3层。

  • 直接子元素选择器 (>): 当你需要确保元素是某个容器的直接子元素时使用。

    # 只选择#sidebar下的直接子菜单项,不包含孙子项
    menu_items = page.locator('#sidebar > .menu-item')
    
  • 相邻兄弟选择器 (+) 和通用兄弟选择器 (~): 适用于基于元素相对位置的定位。

    # 选择紧跟在h2标题后面的第一个段落
    intro_para = page.locator('h2.section-title + p')
    

CSS选择器还能轻松匹配元素的状态,这在处理表单和交互组件时非常有用:

# 定位被选中的复选框
checked_box = page.locator('input[type="checkbox"]:checked')

# 定位当前获得焦点的输入框
focused_input = page.locator('input:focus')

# 定位不可用的提交按钮
disabled_button = page.locator('button[type="submit"]:disabled')

这些状态伪类让测试脚本能更真实地模拟用户操作流,比如先检查按钮是否可点击,再执行点击操作。

3. XPath:应对极端复杂场景的终极武器

虽然我推荐优先使用CSS选择器,但有些场景下XPath是无法替代的。XPath的强大在于它能基于文本内容元素在文档中的位置以及复杂的逻辑关系进行定位。不过,能力越大责任越大,滥用XPath也是测试脚本脆弱的常见根源。

3.1 文本定位:何时用,怎么用?

通过文本定位元素是最符合人类直觉的方式,但也是风险最高的方式之一。Playwright提供了get_by_text()方法,底层其实也是XPath。直接使用文本定位的主要问题在于:

  1. 国际化:页面支持多语言时,文本内容会变化。
  2. 动态文本:如“欢迎回来,张三!”中的用户名部分。
  3. 空格和格式:肉眼看到的文本和DOM中的实际文本可能有差异(如换行符、多余空格)。

我的经验法则是:仅对静态的、关键的业务文本使用文本定位,并且尽量使用部分匹配而非完全匹配。

# 风险较高:完全匹配长文本
page.get_by_text('欢迎来到我们的产品介绍页面,请仔细阅读以下条款。').click()

# 更稳健:匹配关键部分
page.get_by_text('产品介绍').click()

# 或者使用XPath的contains()函数进行部分文本匹配
page.locator('//button[contains(text(), "提交")]').click()

对于多语言项目,一个实用的技巧是将测试数据与定位逻辑分离。你可以维护一个文本映射字典:

BUTTON_TEXTS = {
    'zh-CN': {'submit': '提交', 'cancel': '取消'},
    'en-US': {'submit': 'Submit', 'cancel': 'Cancel'}
}

current_lang = 'zh-CN'
submit_button = page.get_by_text(BUTTON_TEXTS[current_lang]['submit'])

3.2 轴(Axis):XPath的杀手锏

当页面结构复杂,元素缺乏明显特征时,XPath的轴(Axis)功能能帮你建立元素间的相对关系定位。这是XPath最强大也最容易被误用的部分。

  • following-siblingpreceding-sibling: 定位同级元素。

    # 找到“用户名”标签后面的第一个输入框
    username_input = page.locator('//label[text()="用户名"]/following-sibling::input[1]')
    
  • ancestordescendant: 在祖先和后代中导航。

    # 找到包含“价格”文本的div的所有子元素中的span
    price_span = page.locator('//div[contains(text(), "价格")]//descendant::span')
    
  • parent: 选择父元素,常用于事件委托或查找容器。

    # 点击某个特定图标所在的卡片
    card = page.locator('//svg[@data-icon="star"]/parent::div/parent::div[@class="card"]')
    card.click()
    

使用轴定位时,最关键的是找到那个“锚点”——一个稳定且容易定位的元素,然后从这个锚点出发去找到目标元素。这个锚点应该具备以下特征:

  1. 在业务流程中位置固定
  2. 有唯一或高度可识别的属性
  3. 与目标元素的相对位置关系稳定

3.3 性能优化:减少XPath的查询开销

复杂的XPath表达式可能带来性能问题,特别是在大型单页应用中。以下是一些优化建议:

  1. 尽量使用相对路径(//)而非绝对路径(/html/body/...:绝对路径不仅脆弱,而且每次查询都要从根节点开始遍历。
  2. 尽早使用谓词过滤:将最具体的条件放在前面,减少需要检查的节点数。
# 较差:先找到所有div,再过滤
//div[contains(@class, 'container')]/button[@id='submit']

# 较好:直接定位到button,再检查祖先
//button[@id='submit' and ancestor::div[contains(@class, 'container')]]
  1. 避免使用//在深层级中过度搜索:如//div//span//a可能在大型页面中匹配数千个元素。
  2. 考虑将复杂XPath拆分为多个定位步骤:有时先定位到一个中间容器,再在这个容器的上下文中查找目标元素,反而更清晰且高效。

4. Playwright专属定位器:更语义化的选择

从Playwright 1.27版本开始,官方推荐使用一组更语义化的定位器方法,如get_by_role()get_by_label()等。这些方法不是新的选择器语法,而是基于ARIA角色、可访问性属性等标准的最佳实践封装。

4.1 按角色(Role)定位:拥抱可访问性

get_by_role()是我现在最常用的定位方法之一。它通过ARIA角色(如buttontextboxlink)来定位元素,这有几个显著优势:

  • 稳定性高:ARIA角色通常与元素的功能绑定,而不仅仅是样式。一个提交按钮即使CSS类从btn-primary改成primary-button,它的button角色也不会变。
  • 促进可访问性:使用基于角色的定位,无形中也在测试页面的可访问性。如果某个元素无法通过角色定位,可能意味着它缺乏适当的ARIA属性,这对残障用户可能不友好。
  • 意图清晰page.get_by_role('button', name='提交')page.locator('.btn.submit')更能表达“我要找一个名为‘提交’的按钮”这个意图。

常见的ARIA角色包括:

# 定位按钮
submit_btn = page.get_by_role('button', name='提交')

# 定位文本输入框
search_input = page.get_by_role('textbox', name='搜索')

# 定位链接
detail_link = page.get_by_role('link', name='查看详情')

# 定位标题
section_title = page.get_by_role('heading', name='用户信息')

get_by_role()的第二个参数可以接受一个字典,用于指定更多属性,如checkedselected状态:

# 定位被选中的单选按钮
selected_radio = page.get_by_role('radio', checked=True)

# 定位展开的树节点
expanded_treeitem = page.get_by_role('treeitem', expanded=True)

4.2 其他语义化定位器的最佳实践

除了get_by_role(),Playwright还提供了其他几种语义化定位器,各有其适用场景:

  • get_by_label():通过关联的<label>元素定位表单控件。这是定位表单字段最可靠的方式之一,因为labelfor属性或包裹关系通常是稳定的。

    # 定位与“邮箱地址”标签关联的输入框
    email_input = page.get_by_label('邮箱地址')
    
  • get_by_placeholder():通过占位文本定位。适用于没有明确标签的输入框,但要注意占位文本可能会因设计调整而改变。

    # 定位占位文本为“请输入关键词”的搜索框
    search_box = page.get_by_placeholder('请输入关键词')
    
  • get_by_alt_text():通过图片的alt属性定位。不仅用于<img>标签,也适用于有aria-labelalt属性的其他元素。

    # 定位alt文本为“用户头像”的图片
    avatar = page.get_by_alt_text('用户头像')
    
  • get_by_title():通过title属性定位。通常用于带有工具提示的元素。

这些语义化定位器的共同特点是:它们基于元素的功能含义,而非视觉表现。这使你的测试脚本更能适应前端实现细节的变化。

5. 复合定位与等待策略:构建真正稳定的测试

在实际项目中,很少有一个定位器能解决所有问题。更多时候,我们需要组合多种策略,并配合恰当的等待机制,才能应对真实的复杂场景。

5.1 定位器的链式调用与过滤

Playwright允许你对定位结果进行进一步的过滤和操作,这是处理元素列表或需要多重条件定位时的强大工具。

  • filter(): 从一组定位结果中筛选出符合条件的子集。

    # 找到所有可见的提交按钮
    visible_submit_buttons = page.locator('button[type="submit"]').filter(has=page.locator(':visible'))
    
    # 找到包含特定文本的错误消息
    error_message = page.locator('.alert').filter(has_text='密码错误')
    
  • firstlastnth: 选择集合中的特定位置元素。

    # 选择表格第一行
    first_row = page.locator('table tr').first
    
    # 选择第三个选项卡
    third_tab = page.locator('.tab-item').nth(2)  # 索引从0开始
    
  • and_or_: 组合多个定位条件(实验性API,请检查最新文档)。

    # 定位既是按钮又有特定data属性的元素
    special_button = page.get_by_role('button').and_(page.locator('[data-action="special"]'))
    

一个常见的复合定位场景是数据表格操作:你需要先找到包含特定数据的行,然后操作该行中的某个按钮。

# 假设有一个用户表格,要删除名为“张三”的用户
target_row = page.locator('table tr').filter(has_text='张三')
delete_btn_in_row = target_row.locator('button.delete')
delete_btn_in_row.click()

5.2 智能等待:让定位“耐心”一点

即使选择了最稳健的定位器,如果时机不对,依然会失败。Playwright提供了多种等待机制,你需要根据场景选择合适的一种。

  • 隐式等待(不推荐):Playwright的设计哲学是避免全局隐式等待,因为这会拖慢所有操作,并掩盖真正的时序问题。

  • 自动等待(推荐):这是Playwright的核心优势。当你执行click()fill()等操作时,Playwright会自动等待元素满足可操作条件(可见、可交互、稳定等)。你只需要确保定位器是准确的。

  • 显式等待:对于非标准的加载状态,你需要使用wait_for_selector()expect()断言。

    # 等待某个元素出现(最多10秒)
    await page.wait_for_selector('.loading-spinner', state='hidden', timeout=10000)
    
    # 使用expect断言元素可见
    from playwright.sync_api import expect
    expect(page.locator('#success-message')).to_be_visible()
    
  • 自定义等待条件:对于复杂的异步场景,你可能需要等待特定条件成立。

    # 等待直到某个元素包含特定文本
    def wait_for_text(selector, text, timeout=10000):
        start_time = time.time()
        while time.time() - start_time < timeout / 1000:
            element = page.locator(selector)
            if element.count() > 0 and text in element.first.inner_text():
                return True
            time.sleep(0.5)
        return False
    
    # 使用自定义等待
    assert wait_for_text('.status', '同步完成'), "状态未更新为'同步完成'"
    

5.3 定位器的最佳实践清单

根据我在多个项目中的经验,以下这些实践能显著提升定位器的稳定性和可维护性:

  1. 建立定位器策略规范:与团队约定定位器优先级,例如:

    • 第一优先级:data-testid等测试专用属性
    • 第二优先级:get_by_role()get_by_label()等语义化定位器
    • 第三优先级:稳定的CSS选择器(如基于功能而非样式的类名)
    • 最后选择:XPath(仅在必要时使用)
  2. 使用Page Object模式:将定位器与操作逻辑分离,集中管理定位器。当页面变化时,你只需要在一个地方修改。

    class LoginPage:
        def __init__(self, page):
            self.page = page
            self.username_input = page.get_by_label('用户名')
            self.password_input = page.get_by_label('密码')
            self.submit_button = page.get_by_role('button', name='登录')
        
        def login(self, username, password):
            self.username_input.fill(username)
            self.password_input.fill(password)
            self.submit_button.click()
    
  3. 为定位器添加描述性名称:即使使用Page Object,也要给定位器起一个能反映其用途的名字,而不是简单描述其外观。

  4. 定期审查定位器:在迭代过程中,定期检查是否有定位器变得脆弱或冗余。特别是当页面进行较大重构时,要同步更新定位器策略。

  5. 利用Playwright的调试工具playwright inspectortrace viewer能帮你直观地看到定位器是如何匹配元素的,这对于调试复杂的定位问题非常有用。

6. 实战:复杂场景下的定位解决方案

理论说再多,不如看几个真实场景。下面我分享几个在项目中遇到的典型难题及其解决方案。

6.1 动态ID与随机类名

很多前端框架(如React、Vue)会为元素生成随机的ID或类名,这给定位带来了巨大挑战。面对这种情况,我的策略是:

向上寻找稳定的容器:如果元素本身属性不稳定,就向上查找其父级或祖先元素中是否有稳定的容器。

# 假设每个列表项都有随机生成的ID,但其容器有稳定的data属性
list_container = page.locator('[data-component="user-list"]')

# 在容器内相对定位
first_user = list_container.locator('.user-item').first
edit_button = first_user.locator('button:has-text("编辑")')

使用属性部分匹配:有时随机生成的值中仍有规律可循,如都以user-开头。

# 匹配ID以“user-”开头的所有元素
user_elements = page.locator('[id^="user-"]')

与开发团队协作:这是最根本的解决方案。推动团队为自动化测试添加稳定的测试属性,如data-testid。这不仅能提升测试稳定性,还能促进开发与测试的协作。

6.2 iframe与Shadow DOM中的元素

iframe和Shadow DOM将元素隔离在独立的文档上下文中,需要特殊处理。

iframe处理

# 定位iframe本身
iframe = page.frame_locator('iframe[name="content"]')

# 在iframe上下文中定位元素
iframe.locator('#submit-btn').click()

Shadow DOM处理: Playwright可以穿透Shadow DOM,但语法略有不同:

# 定位Shadow Host
host = page.locator('custom-element')

# 穿透Shadow DOM定位内部元素
shadow_button = host.locator('>>> .internal-button')
# 或者使用Piercing选择器(如果浏览器支持)
shadow_button = host.locator('pierce/.internal-button')

6.3 表格数据的精准操作

操作表格中的特定行是自动化测试中的常见需求。关键在于如何唯一地标识目标行。

方案一:基于行内数据定位

# 找到包含“张三”且状态为“活跃”的行
target_row = page.locator('table tr').filter(
    has=page.locator('td:has-text("张三")')
).filter(
    has=page.locator('td:has-text("活跃")')
)

# 操作该行中的按钮
target_row.locator('button.edit').click()

方案二:使用行索引(谨慎使用) 如果表格顺序稳定,且测试不关心具体数据,可以使用索引:

# 操作第二行(索引从0开始)
second_row = page.locator('table tr').nth(1)
second_row.locator('input[type="checkbox"]').check()

方案三:结合表头定位特定列

# 找到“操作”列下的所有按钮
action_column_index = None
headers = page.locator('table thead th').all()
for i, header in enumerate(headers):
    if header.inner_text() == '操作':
        action_column_index = i
        break

if action_column_index is not None:
    # 每行中对应列的操作按钮
    action_buttons = page.locator(f'table tbody tr td:nth-child({action_column_index + 1}) button')

6.4 文件上传与复杂表单

文件上传是Web自动化中的特殊场景,因为<input type="file">元素通常被隐藏或样式化。

标准文件上传

# 定位文件输入元素(即使它被隐藏)
file_input = page.locator('input[type="file"]')

# 设置文件路径
file_input.set_input_files('/path/to/file.pdf')

# 上传多个文件
file_input.set_input_files(['/path/to/file1.pdf', '/path/to/file2.jpg'])

复杂表单的填充策略: 对于包含多种输入类型的表单,建议按字段类型采用不同的定位策略:

# 文本输入框:使用get_by_label()或get_by_placeholder()
page.get_by_label('用户名').fill('testuser')
page.get_by_placeholder('请输入邮箱').fill('test@example.com')

# 单选按钮和复选框:使用get_by_role()结合label
page.get_by_role('radio', name='男').check()
page.get_by_role('checkbox', name='同意条款').check()

# 下拉选择框:使用select_option()
page.locator('select#country').select_option('CN')

# 日期选择器:直接填充或使用专用组件方法
page.locator('input[type="date"]').fill('2024-01-15')

7. 调试与维护:让定位问题无处遁形

即使有了完善的定位策略,在实际运行中仍然可能遇到问题。建立一个高效的调试和维护流程,比写出完美的定位器更重要。

7.1 定位器调试技巧

当定位器失效时,不要急于修改代码,先按以下步骤排查:

  1. 在Playwright Inspector中实时测试

    # 以调试模式运行脚本
    PWDEBUG=1 python your_script.py
    

    这会打开Playwright Inspector,你可以实时查看页面,测试不同的定位器,并查看它们匹配到了哪些元素。

  2. 使用evaluate()检查元素状态

    # 检查元素是否真的在页面上
    element = page.locator('#my-button')
    is_visible = element.evaluate('el => el.offsetParent !== null')
    print(f"元素可见: {is_visible}")
    
    # 获取元素的所有属性
    attributes = element.evaluate('el => {
        const attrs = {};
        for (const attr of el.attributes) {
            attrs[attr.name] = attr.value;
        }
        return attrs;
    }')
    print(attributes)
    
  3. 截图辅助调试

    # 在操作前后截图
    page.screenshot(path='before_click.png')
    element.click()
    page.screenshot(path='after_click.png')
    
    # 只截图特定元素
    element.screenshot(path='element_only.png')
    

7.2 定位器维护策略

随着项目迭代,页面会不断变化,定位器也需要相应维护。以下策略可以帮助减少维护成本:

  1. 建立定位器仓库:将常用的、跨页面的定位器集中管理,避免重复定义。

    # locators/common.py
    class CommonLocators:
        @staticmethod
        def modal_submit_button(page):
            return page.get_by_role('button', name='确定')
        
        @staticmethod  
        def loading_spinner(page):
            return page.locator('.ant-spin-dot')
    
    # 在测试中使用
    from locators.common import CommonLocators
    CommonLocators.modal_submit_button(page).click()
    
  2. 实现定位器版本兼容:对于重要的元素,可以准备多个备选定位器,按优先级尝试。

    def robust_locate_submit_button(page):
        # 按优先级尝试不同的定位策略
        selectors = [
            lambda: page.get_by_role('button', name='提交'),
            lambda: page.locator('[data-testid="submit-btn"]'),
            lambda: page.locator('button.primary:has-text("提交")'),
            lambda: page.locator('//button[contains(text(), "提交")]')
        ]
        
        for selector_func in selectors:
            try:
                element = selector_func()
                if element.count() == 1:
                    return element
            except:
                continue
        
        raise Exception("无法定位提交按钮")
    
  3. 定期运行定位器健康检查:编写一个简单的脚本,定期检查所有定位器是否仍然有效,及时发现潜在问题。

7.3 性能监控与优化

定位器的性能直接影响测试执行速度。对于大型测试套件,优化定位器可以节省大量时间。

  • 避免过度使用//全局搜索:特别是在大型页面中,尽量缩小搜索范围。
  • 缓存定位器结果:如果同一个元素在多个地方使用,可以将其缓存起来。
    class LoginPage:
        def __init__(self, page):
            self.page = page
            self._submit_button = None  # 延迟初始化
        
        @property
        def submit_button(self):
            if self._submit_button is None:
                self._submit_button = self.page.get_by_role('button', name='登录')
            return self._submit_button
    
  • 使用locator()链式调用而非重复查询
    # 较差:多次查询
    table = page.locator('table.data-table')
    first_row = page.locator('table.data-table tr').first
    first_cell = page.locator('table.data-table tr').first.locator('td').first
    
    # 较好:链式调用
    table = page.locator('table.data-table')
    first_row = table.locator('tr').first
    first_cell = first_row.locator('td').first
    

最后,我想强调的是,元素定位不是一门孤立的技能,它与你对应用架构、前端技术和测试设计的理解密切相关。在我经历的项目中,最稳定的测试套件往往不是因为有最精巧的定位器,而是因为测试代码与产品代码共同演进,形成了良好的协作模式。当你发现某个定位器频繁失效时,这可能不只是自动化测试的问题,而是产品可测试性需要改进的信号。与开发团队一起建立可测试性规范,比如约定使用data-testid属性,长远来看会让所有人的工作都更轻松。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐