HarmonyOS开发:组件查找——By选择器
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"。
解决方案:
- 优先用id查找,不依赖文本
- 如果必须用text,把文案抽成常量,根据语言环境切换
// 根据语言环境选择查找文本
const LOGIN_TEXT = currentLanguage === 'zh' ? '登录' : 'Login';
const loginBtn = driver.findComponent(ON.text(LOGIN_TEXT));
坑3:findComponents的性能问题
findComponents会遍历整个组件树,如果页面复杂,这个操作可能很慢。特别是嵌套层级很深的页面,一次查找可能要几百毫秒。
建议:
- 能用id精确查找就别用findComponents遍历
- 如果必须遍历,先通过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选择器做了以下增强:
- 新增ON.key属性查找:可以通过组件的key属性查找,适合动态列表中标识特定项
- ON.isEnabled/isFocused状态查找:可以根据组件的可用/聚焦状态筛选,比如只找可点击的按钮
- 组合选择器支持OR逻辑:之前只有AND,现在支持
A.OR(B),满足任一条件即可 - 查找性能优化:组件树遍历算法优化,深层嵌套场景查找速度提升约40%
- 新增ON.isDisplayed查找:只匹配实际可见的组件,过滤掉被遮挡或visibility=none的组件
迁移注意:ON.textRegex方法在API 13中被废弃,统一使用ON.textMatches,参数格式不变。
总结
By选择器是UI测试的地基。找组件找不准,后面所有操作都是白搭。核心原则就一个:能用id就别用text,能精确就别模糊,能局部就别全局。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐ 概念简单,组合查找需要练习 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每个UI测试都要用 |
| 重要程度 | ⭐⭐⭐⭐⭐ 直接决定测试稳定性 |
最后给个忠告:别把选择器写死在测试代码里。今天ON.text('提交')能用,明天产品改成ON.text('确认提交')你就得改所有测试。把选择器封装到Locator类里,改一处全局生效——这才是长期维护的正确姿势。
- 点赞
- 收藏
- 关注作者
评论(0)