HarmonyOS开发:组件查找——By选择器

举报
Jack20 发表于 2026/06/24 15:40:04 2026/06/24
【摘要】 HarmonyOS开发:组件查找——By选择器📌 核心要点:By选择器是UiTest框架的"眼睛",通过id、text、type、description等维度精准定位UI组件,支持组合查找、层级查找和模糊匹配,找对组件是写好UI测试的第一步。 背景与动机上一篇文章我们搭了UiTest框架的基本架子,知道了Driver是大脑、Component是目标。但有个问题一直没展开——你怎么找到你要...

HarmonyOS开发:组件查找——By选择器

📌 核心要点:By选择器是UiTest框架的"眼睛",通过id、text、type、description等维度精准定位UI组件,支持组合查找、层级查找和模糊匹配,找对组件是写好UI测试的第一步。

背景与动机

上一篇文章我们搭了UiTest框架的基本架子,知道了Driver是大脑、Component是目标。但有个问题一直没展开——你怎么找到你要操作的那个组件?

页面上几十个组件,按钮、文本、图片、列表……你告诉Driver"我要点登录按钮",它怎么知道哪个是登录按钮?靠猜?靠数第几个?

当然不是。你得给它一个查找规则——这就是By选择器干的事。

By选择器就像CSS选择器,你用一套规则描述"我要找什么样的组件",框架帮你去页面上匹配。找得准,后续操作才靠谱;找不准,整个测试就是空中楼阁。

但现实往往没那么简单。你的页面可能没有给每个组件设id,按钮上的文字可能随时改,列表项的type全都一样……这时候怎么找?这就是这篇文章要解决的问题。

核心原理

By选择器的查找维度

UiTest框架提供了ON对象(本质就是By选择器的工厂方法),支持以下查找维度:

graph TB
    A[ON 选择器] --> B[id 精确匹配]
    A --> C[text 文本匹配]
    A --> D[type 组件类型]
    A --> E[description 描述匹配]
    A --> F[组合选择器]
    A --> G[层级选择器]
    
    B --> B1["ON.id('login_btn')"]
    C --> C1["ON.text('登录')"]
    C --> C2["ON.textContains('录')"]
    D --> D1["ON.type('Button')"]
    E --> E1["ON.description('提交表单')"]
    F --> F1["ON.id('btn').AND(ON.type('Button'))"]
    G --> G1["ON.id('list').children<ON.type('ListItem')>"]
    
    classDef root fill:#4A90D9,stroke:#2C5F8A,color:#fff
    classDef basic fill:#67C23A,stroke:#3E9B2B,color:#fff
    classDef example fill:#E6A23C,stroke:#B07D2B,color:#fff
    classDef combo fill:#F56C6C,stroke:#C94A4A,color:#fff
    classDef hier fill:#9B59B6,stroke:#7D3C98,color:#fff
    
    class A root
    class B,C,D,E basic
    class F combo
    class G hier
    class B1,C1,C2,D1,E1,F1,G1 example

查找策略的优先级

选择哪个维度查找,直接影响测试的稳定性和可维护性。优先级从高到低:

优先级 维度 稳定性 说明
1 id ⭐⭐⭐⭐⭐ 最稳定,id是开发时设定的,不会随UI调整变化
2 description ⭐⭐⭐⭐ 无障碍描述,语义明确,但不是每个组件都设了
3 type + 组合 ⭐⭐⭐ 类型稳定,但需要组合其他条件缩小范围
4 text ⭐⭐ 文本可能因国际化、需求变更而改变
5 层级查找 布局一改就挂,是最后的手段

记住这个优先级。能用id就用id,别偷懒用text——你今天用text写的测试,明天产品改了个文案就全挂了。

查找流程

当你调用driver.findComponent(ON.id('xxx'))时,框架内部做了什么?

flowchart TD
    A[调用findComponent] --> B[解析选择器条件]
    B --> C[遍历组件树]
    C --> D{匹配条件?}
    D -->|| E[返回第一个匹配的Component]
    D -->|| F{还有未遍历节点?}
    F -->|| C
    F -->|| G[返回null]
    
    classDef start fill:#4A90D9,stroke:#2C5F8A,color:#fff
    classDef process fill:#67C23A,stroke:#3E9B2B,color:#fff
    classDef decision fill:#E6A23C,stroke:#B07D2B,color:#fff
    classDef success fill:#67C23A,stroke:#3E9B2B,color:#fff
    classDef fail fill:#F56C6C,stroke:#C94A4A,color:#fff
    
    class A start
    class B,C process
    class D,F decision
    class E success
    class G fail

关键点:findComponent返回的是第一个匹配的组件。如果页面上有多个同id的组件(虽然不应该有),你拿到的是组件树遍历顺序中第一个找到的。

代码实战

基础用法:四种基本查找方式

// 462_by_selector_basic.ets
import { Driver, ON } from '@ohos.UiTest';

export default function basicSelectorTest() {
  const driver = Driver.create();
  
  // ===== 1. 按id查找(最推荐) =====
  // 前提:开发时给组件设了id
  // <Button id="login_btn">登录</Button>
  const loginBtn = driver.findComponent(ON.id('login_btn'));
  console.info(`按id查找: ${loginBtn !== null ? '找到' : '未找到'}`);
  
  // ===== 2. 按text查找 =====
  // 直接匹配按钮上的文字
  // 注意:text是精确匹配,"登录"不会匹配"登录按钮"
  const loginByText = driver.findComponent(ON.text('登录'));
  console.info(`按text查找: ${loginByText !== null ? '找到' : '未找到'}`);
  
  // ===== 3. 按type查找 =====
  // type是组件的类型名,如Button、Text、Image等
  // 单独用type查找范围太广,通常需要组合其他条件
  const allButtons = driver.findComponents(ON.type('Button'));
  console.info(`页面上共有 ${allButtons.length} 个Button`);
  
  // ===== 4. 按description查找 =====
  // description是无障碍描述,对应accessibilityText属性
  // <Button accessibilityText="提交登录表单">登录</Button>
  const loginByDesc = driver.findComponent(ON.description('提交登录表单'));
  console.info(`按description查找: ${loginByDesc !== null ? '找到' : '未找到'}`);
}

四种方式各有适用场景。id最稳但需要开发配合设值;text最直觉但容易因文案变更而失效;type范围太广需要组合;description最语义化但覆盖率低。

进阶用法:组合选择器与模糊匹配

单个条件找不到?那就组合起来用。

// 462_by_selector_advanced.ets
import { Driver, ON, MatchPattern } from '@ohos.UiTest';

export default function advancedSelectorTest() {
  const driver = Driver.create();
  
  // ===== 1. 组合选择器:AND条件 =====
  // 页面上有多个Button,但你要找的是id为"submit"的那个Button
  // ON.id('submit').AND(ON.type('Button'))
  // 注意:AND方法把两个条件组合起来,必须同时满足
  const submitBtn = driver.findComponent(
    ON.id('submit').AND(ON.type('Button'))
  );
  
  // 更实用的场景:同一个type下区分不同实例
  // 比如页面上有多个Text,你要找显示"价格"的那个
  const priceText = driver.findComponent(
    ON.type('Text').AND(ON.textContains('价格'))
  );
  
  // ===== 2. 模糊匹配 =====
  // textContains:包含指定文本
  // "商品价格:¥99" 包含 "价格"
  const partialText = driver.findComponent(ON.textContains('价格'));
  
  // textStartsWith:以指定文本开头
  // "商品价格:¥99" 以 "商品" 开头
  const startText = driver.findComponent(ON.textStartsWith('商品'));
  
  // ===== 3. 正则匹配(更灵活的模糊匹配) =====
  // MatchPattern支持正则表达式
  // 匹配 "价格:¥99" 或 "价格:¥199" 等
  const regexText = driver.findComponent(
    ON.textMatches('价格:¥\\d+')
  );
  
  // ===== 4. 层级查找 =====
  // 先找父组件,再在子组件中查找
  // 场景:一个列表里有很多ListItem,每个ListItem里有个删除按钮
  // 你要找特定ListItem里的删除按钮
  
  // 方法1:先找父组件,再用children查找
  const list = driver.findComponent(ON.id('product_list'));
  if (list !== null) {
    // 在list的子组件中查找type为ListItem的所有组件
    const items = list.findComponents(ON.type('ListItem'));
    console.info(`列表中有 ${items.length} 个ListItem`);
    
    // 在第一个ListItem中查找删除按钮
    if (items.length > 0) {
      const deleteBtn = items[0].findComponent(ON.text('删除'));
      deleteBtn?.click();
    }
  }
  
  // 方法2:ON的isBefore/isAfter辅助定位
  // 找到"价格"文本之后紧挨着的Text组件
  const afterPrice = driver.findComponent(
    ON.type('Text').isAfter(ON.textContains('价格'))
  );
  
  // ===== 5. 查找所有匹配组件 =====
  // findComponents(注意复数)返回所有匹配的组件数组
  const allListItems = driver.findComponents(ON.type('ListItem'));
  console.info(`共找到 ${allListItems.length} 个ListItem`);
  
  // 遍历查找特定内容的ListItem
  for (const item of allListItems) {
    const itemText = item.findComponent(ON.type('Text'));
    if (itemText !== null) {
      const text = itemText.getText();
      if (text.includes('目标商品')) {
        console.info('找到目标商品所在的ListItem');
        break;
      }
    }
  }
}

完整示例:电商页面的组件查找实战

来看一个真实的电商商品详情页,练习各种查找策略的综合运用:

// 462_by_selector_full.ets
import { Driver, ON, Component } from '@ohos.UiTest';

// 商品详情页的组件查找封装
class ProductPageLocator {
  private driver: Driver;
  
  constructor(driver: Driver) {
    this.driver = driver;
  }
  
  // 商品图片轮播
  getImageViewer(): Component | null {
    return this.driver.findComponent(ON.id('product_images'));
  }
  
  // 商品名称
  getProductName(): string {
    const name = this.driver.findComponent(ON.id('product_name'));
    return name?.getText() ?? '';
  }
  
  // 商品价格(模糊匹配——价格会变动)
  getProductPrice(): string {
    // 价格文本格式可能是 "¥99.00" 或 "¥199.00"
    // 用textStartsWith匹配
    const price = this.driver.findComponent(ON.textStartsWith('¥'));
    return price?.getText() ?? '';
  }
  
  // 规格选择按钮(组合查找)
  getSizeOption(sizeName: string): Component | null {
    // 在规格区域内查找指定文字的按钮
    const specArea = this.driver.findComponent(ON.id('spec_area'));
    if (specArea === null) return null;
    
    return specArea.findComponent(ON.text(sizeName));
  }
  
  // 加入购物车按钮
  getAddToCartBtn(): Component | null {
    // 优先用id,其次用text兜底
    let btn = this.driver.findComponent(ON.id('add_to_cart'));
    if (btn === null) {
      btn = this.driver.findComponent(
        ON.type('Button').AND(ON.textContains('加入购物车'))
      );
    }
    return btn;
  }
  
  // 立即购买按钮
  getBuyNowBtn(): Component | null {
    return this.driver.findComponent(ON.id('buy_now'));
  }
  
  // 商品评价列表中的第N条评价
  getReviewItem(index: number): Component | null {
    const reviewList = this.driver.findComponent(ON.id('review_list'));
    if (reviewList === null) return null;
    
    const items = reviewList.findComponents(ON.type('ListItem'));
    return index < items.length ? items[index] : null;
  }
  
  // 收藏按钮(根据状态区分)
  getFavoriteBtn(isFavorited: boolean): Component | null {
    // 未收藏状态显示"收藏",已收藏显示"已收藏"
    const text = isFavorited ? '已收藏' : '收藏';
    return this.driver.findComponent(
      ON.id('favorite_btn').AND(ON.textContains(text))
    );
  }
}

// 测试用例
export default function productPageTest() {
  const driver = Driver.create();
  const page = new ProductPageLocator(driver);
  
  // 验证页面基本元素
  const productName = page.getProductName();
  console.info(`商品名称: ${productName}`);
  
  const price = page.getProductPrice();
  console.info(`商品价格: ${price}`);
  
  // 选择规格
  const xlOption = page.getSizeOption('XL');
  if (xlOption !== null) {
    xlOption.click();
    console.info('已选择XL规格');
  }
  
  // 加入购物车
  const addBtn = page.getAddToCartBtn();
  if (addBtn !== null) {
    addBtn.click();
    console.info('已点击加入购物车');
  }
  
  // 查看评价
  const firstReview = page.getReviewItem(0);
  if (firstReview !== null) {
    console.info('找到第一条评价');
  }
  
  // 收藏
  const favBtn = page.getFavoriteBtn(false);
  favBtn?.click();
  console.info('已收藏商品');
}

这个例子把查找逻辑封装到了Locator类里,测试代码只关心业务流程,不关心底层怎么找组件。这就是Page Object模式的雏形,后面第470篇会详细讲。

踩坑与注意事项

坑1:id没设,啥都找不到

这是最常见的问题。很多开发者写UI的时候根本不给组件设id,导致测试时只能用text或type查找,稳定性大打折扣。

解决方案:从项目一开始就约定好——所有需要测试的组件必须设id。这不是可选项,是团队规范。

// ❌ 没有id,测试只能靠text找
Button('登录')

// ✅ 设了id,测试稳如老狗
Button('登录').id('login_btn')

坑2:text匹配的国际化问题

你的应用支持多语言吧?中文环境下ON.text('登录')能找到按钮,切到英文环境就找不到了——因为按钮上显示的是"Login"。

解决方案

  1. 优先用id查找,不依赖文本
  2. 如果必须用text,把文案抽成常量,根据语言环境切换
// 根据语言环境选择查找文本
const LOGIN_TEXT = currentLanguage === 'zh' ? '登录' : 'Login';
const loginBtn = driver.findComponent(ON.text(LOGIN_TEXT));

坑3:findComponents的性能问题

findComponents会遍历整个组件树,如果页面复杂,这个操作可能很慢。特别是嵌套层级很深的页面,一次查找可能要几百毫秒。

建议

  1. 能用id精确查找就别用findComponents遍历
  2. 如果必须遍历,先通过id找到父容器,再在子树中查找
// ❌ 全局遍历,慢
const items = driver.findComponents(ON.type('ListItem'));

// ✅ 先定位父容器,再在子树中查找
const list = driver.findComponent(ON.id('my_list'));
const items = list?.findComponents(ON.type('ListItem'));

坑4:组件树的动态变化

LazyForEach渲染的列表,组件树是动态的。不在可视区域的组件根本不存在于组件树中,你用任何选择器都找不到。

解决方案:先滚动到目标位置,再查找。

// 先找到列表组件
const list = driver.findComponent(ON.id('product_list'));
// 滚动到底部
list?.scrollSearch(ON.text('最后一项'));
// 现在才能找到
const lastItem = driver.findComponent(ON.text('最后一项'));

坑5:组合选择器的顺序

A.AND(B)B.AND(A)逻辑上是等价的,但实际执行效率可能不同。框架会先按第一个条件筛选,再在结果中匹配第二个条件。所以把更精确的条件放前面,可以减少第二次匹配的范围。

// ✅ 先用id缩小范围(id唯一,结果只有1个),再验证type
ON.id('login_btn').AND(ON.type('Button'))

// ❌ 先用type筛选(页面上可能几十个Button),再逐个检查id
ON.type('Button').AND(ON.id('login_btn'))

HarmonyOS 6适配说明

HarmonyOS 6对By选择器做了以下增强:

  1. 新增ON.key属性查找:可以通过组件的key属性查找,适合动态列表中标识特定项
  2. ON.isEnabled/isFocused状态查找:可以根据组件的可用/聚焦状态筛选,比如只找可点击的按钮
  3. 组合选择器支持OR逻辑:之前只有AND,现在支持A.OR(B),满足任一条件即可
  4. 查找性能优化:组件树遍历算法优化,深层嵌套场景查找速度提升约40%
  5. 新增ON.isDisplayed查找:只匹配实际可见的组件,过滤掉被遮挡或visibility=none的组件

迁移注意:ON.textRegex方法在API 13中被废弃,统一使用ON.textMatches,参数格式不变。

总结

By选择器是UI测试的地基。找组件找不准,后面所有操作都是白搭。核心原则就一个:能用id就别用text,能精确就别模糊,能局部就别全局

维度 评价
学习难度 ⭐⭐ 概念简单,组合查找需要练习
使用频率 ⭐⭐⭐⭐⭐ 每个UI测试都要用
重要程度 ⭐⭐⭐⭐⭐ 直接决定测试稳定性

最后给个忠告:别把选择器写死在测试代码里。今天ON.text('提交')能用,明天产品改成ON.text('确认提交')你就得改所有测试。把选择器封装到Locator类里,改一处全局生效——这才是长期维护的正确姿势。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。