HarmonyOS开发:用户操作模拟——点击与滑动
HarmonyOS开发:用户操作模拟——点击与滑动
核心要点:UiTest框架通过Component的操作方法模拟真实用户交互,涵盖点击、长按、双击、滑动、拖拽、输入文本、按键操作,掌握这些操作才能写出真正覆盖用户行为的测试。
背景与动机
你找到了组件,然后呢?光找到不操作,那叫看戏,不叫测试。
用户怎么用你的App?点按钮、滑列表、输入文字、长按弹出菜单……这些操作你得用代码模拟出来,测试才有意义。不然你只是验证了"页面上有个按钮",没验证"点了按钮之后会发生什么"。
HarmonyOS的UiTest框架提供了完整的用户操作模拟API。从最基础的点击,到复杂的拖拽和手势组合,基本覆盖了用户在设备上能做的所有操作。
但操作模拟有个核心矛盾:你模拟的操作越像真实用户,测试越可靠;但越像真实用户,执行速度越慢。怎么在可靠性和效率之间找平衡,是这篇文章的重点。
核心原理
操作模拟的层次模型
UiTest框架的操作模拟分三个层次:
graph TB
A[操作模拟] --> B[原子操作层]
A --> C[组合操作层]
A --> D[手势操作层]
B --> B1[click 点击]
B --> B2[longClick 长按]
B --> B3[doubleClick 双击]
B --> B4[inputText 输入]
B --> B5[pressKey 按键]
C --> C1[swipe 滑动]
C --> C2[drag 拖拽]
C --> C3[pinch 捏合]
C --> C4[fling 甩动]
D --> D1[自定义手势序列]
D --> D2[多点触控]
D --> D3[手势注入]
classDef root fill:#4A90D9,stroke:#2C5F8A,color:#fff
classDef atomic fill:#67C23A,stroke:#3E9B2B,color:#fff
classDef combo fill:#E6A23C,stroke:#B07D2B,color:#fff
classDef gesture fill:#F56C6C,stroke:#C94A4A,color:#fff
class A root
class B,B1,B2,B3,B4,B5 atomic
class C,C1,C2,C3,C4 combo
class D,D1,D2,D3 gesture
操作执行流程
当你调用component.click()时,框架做了什么?
sequenceDiagram
participant T as 测试脚本
participant D as Driver
participant F as 框架层
participant U as UI线程
T->>D: component.click()
D->>F: 获取组件坐标
F->>F: 计算点击位置(中心点)
F->>U: 注入触摸事件
U->>U: 触发onClick回调
U->>U: 执行状态更新
U->>F: 操作完成
F->>D: 返回结果
D->>T: 操作完成
注意,操作是异步注入的。click()调用返回时,onClick回调可能还没执行完。这就是为什么你click完立即去找下一个组件,可能找不到——UI还没更新呢。
操作的坐标计算
每个操作都基于组件的坐标。框架默认操作组件的中心点,但你也可以指定偏移:
click():点击组件中心click(offsetX, offsetY):点击组件内偏移位置swipe(target):从组件中心滑到目标组件中心swipe(targetX, targetY):从组件中心滑到指定坐标
代码实战
基础用法:点击与输入
// 463_user_action_basic.ets
import { Driver, ON } from '@ohos.UiTest';
export default function basicActionTest() {
const driver = Driver.create();
// ===== 1. 单击 =====
const loginBtn = driver.findComponent(ON.id('login_btn'));
if (loginBtn !== null) {
loginBtn.click(); // 点击组件中心
console.info('已点击登录按钮');
}
// ===== 2. 长按 =====
// 长按通常触发上下文菜单或进入编辑模式
const listItem = driver.findComponent(ON.id('item_1'));
if (listItem !== null) {
listItem.longClick(); // 默认长按500ms
console.info('已长按列表项');
}
// ===== 3. 双击 =====
// 双击通常用于放大/缩小图片
const imageView = driver.findComponent(ON.id('product_image'));
if (imageView !== null) {
imageView.doubleClick();
console.info('已双击图片');
}
// ===== 4. 输入文本 =====
const searchInput = driver.findComponent(ON.id('search_input'));
if (searchInput !== null) {
// inputText会先清空已有内容,再输入新文本
searchInput.inputText('HarmonyOS');
console.info('已输入搜索文本');
}
// ===== 5. 按键操作 =====
// 模拟物理按键
driver.pressKey(KeyCode.BACK); // 返回键
driver.pressKey(KeyCode.HOME); // Home键
driver.pressKey(KeyCode.ENTER); // 回车键
console.info('基础操作测试完成');
}
进阶用法:滑动与拖拽
// 463_user_action_advanced.ets
import { Driver, ON, Component } from '@ohos.UiTest';
export default function advancedActionTest() {
const driver = Driver.create();
// ===== 1. 基础滑动 =====
// 从一个组件滑到另一个组件
const list = driver.findComponent(ON.id('product_list'));
if (list !== null) {
// 向上滑动(查看下方内容)
list.swipe(ON.id('bottom_item'), SwipeDirection.UP);
// 向下滑动(回到顶部)
list.swipe(ON.id('top_item'), SwipeDirection.DOWN);
}
// ===== 2. 按坐标滑动 =====
// 更精确的控制——从组件中心向指定方向滑指定距离
const scrollArea = driver.findComponent(ON.id('scroll_area'));
if (scrollArea !== null) {
// 向上滑动300vp
scrollArea.swipe(SwipeDirection.UP, 300);
// 向左滑动(横向滑动,比如切换Tab)
scrollArea.swipe(SwipeDirection.LEFT, 200);
}
// ===== 3. 拖拽操作 =====
// 把一个组件拖到另一个位置
// 场景:拖拽排序、拖拽到购物车
const dragItem = driver.findComponent(ON.id('draggable_item'));
const dropTarget = driver.findComponent(ON.id('drop_zone'));
if (dragItem !== null && dropTarget !== null) {
dragItem.drag(dropTarget);
console.info('已拖拽到目标位置');
}
// ===== 4. 带偏移的点击 =====
// 点击组件内的特定位置
// 场景:点击按钮的右半部分、点击图片的某个区域
const bigButton = driver.findComponent(ON.id('wide_button'));
if (bigButton !== null) {
// 点击按钮右侧区域(偏移X=80%位置)
const bounds = bigButton.getBounds();
bigButton.click(bounds.width * 0.8, bounds.height * 0.5);
}
// ===== 5. 捏合操作(缩放) =====
const mapView = driver.findComponent(ON.id('map_view'));
if (mapView !== null) {
// 放大
mapView.pinch(PinchDirection.OPEN, 1.5); // 放大1.5倍
// 缩小
mapView.pinch(PinchDirection.CLOSE, 0.5); // 缩小到0.5倍
}
// ===== 6. 甩动操作(快速滑动) =====
// 模拟用户快速滑动后松手,带有惯性
const gallery = driver.findComponent(ON.id('image_gallery'));
if (gallery !== null) {
// 快速向左甩动(切换到下一张图片)
gallery.fling(SwipeDirection.LEFT, 1000); // 速度1000vp/s
}
console.info('进阶操作测试完成');
}
完整示例:电商购物流程的完整操作模拟
// 463_user_action_full.ets
import { Driver, ON, Component } from '@ohos.UiTest';
// 购物流程操作封装
class ShoppingFlowOperator {
private driver: Driver;
constructor() {
this.driver = Driver.create();
}
// 搜索商品
async searchProduct(keyword: string): Promise<boolean> {
// 点击搜索框
const searchBox = this.driver.findComponent(ON.id('search_box'));
if (searchBox === null) return false;
searchBox.click();
// 等待搜索页面加载
await this.waitMoment(500);
// 输入关键词
const searchInput = this.driver.findComponent(ON.id('search_input'));
if (searchInput === null) return false;
searchInput.inputText(keyword);
// 按回车搜索
this.driver.pressKey(KeyCode.ENTER);
// 等待搜索结果
await this.waitMoment(1000);
return true;
}
// 浏览商品列表并选择
async browseAndSelect(itemIndex: number): Promise<boolean> {
// 找到商品列表
const list = this.driver.findComponent(ON.id('product_list'));
if (list === null) return false;
// 如果目标商品不在可视区域,先滑动
const items = list.findComponents(ON.type('ListItem'));
if (itemIndex >= items.length) {
// 向上滑动加载更多
list.swipe(SwipeDirection.UP, 300);
await this.waitMoment(500);
}
// 重新获取列表项
const updatedItems = list.findComponents(ON.type('ListItem'));
if (itemIndex >= updatedItems.length) return false;
// 点击目标商品
updatedItems[itemIndex].click();
await this.waitMoment(1000);
return true;
}
// 选择商品规格
async selectSpec(color: string, size: string): Promise<boolean> {
// 选择颜色
const colorOption = this.driver.findComponent(ON.text(color));
if (colorOption !== null) {
colorOption.click();
await this.waitMoment(300);
}
// 选择尺码
const sizeOption = this.driver.findComponent(ON.text(size));
if (sizeOption !== null) {
sizeOption.click();
await this.waitMoment(300);
}
return true;
}
// 加入购物车
async addToCart(): Promise<boolean> {
const addBtn = this.driver.findComponent(ON.id('add_to_cart'));
if (addBtn === null) return false;
addBtn.click();
await this.waitMoment(1000);
return true;
}
// 进入购物车
async goToCart(): Promise<boolean> {
const cartTab = this.driver.findComponent(ON.id('tab_cart'));
if (cartTab === null) return false;
cartTab.click();
await this.waitMoment(800);
return true;
}
// 结算
async checkout(): Promise<boolean> {
// 勾选商品
const checkbox = this.driver.findComponent(ON.id('cart_item_checkbox'));
if (checkbox !== null) {
checkbox.click();
await this.waitMoment(300);
}
// 点击结算
const checkoutBtn = this.driver.findComponent(ON.text('结算'));
if (checkoutBtn === null) return false;
checkoutBtn.click();
await this.waitMoment(1000);
return true;
}
// 选择支付方式并支付
async pay(): Promise<boolean> {
// 选择微信支付
const wechatPay = this.driver.findComponent(ON.text('微信支付'));
if (wechatPay !== null) {
wechatPay.click();
await this.waitMoment(300);
}
// 确认支付
const confirmBtn = this.driver.findComponent(ON.id('confirm_pay'));
if (confirmBtn === null) return false;
confirmBtn.click();
await this.waitMoment(2000);
return true;
}
// 辅助方法:等待
private waitMoment(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 完整购物流程测试
export default async function shoppingFlowTest() {
const shop = new ShoppingFlowOperator();
console.info('===== 开始购物流程测试 =====');
// 第1步:搜索商品
const searchOk = await shop.searchProduct('蓝牙耳机');
console.info(`搜索商品: ${searchOk ? '✅' : '❌'}`);
if (!searchOk) return;
// 第2步:浏览并选择第一个商品
const selectOk = await shop.browseAndSelect(0);
console.info(`选择商品: ${selectOk ? '✅' : '❌'}`);
if (!selectOk) return;
// 第3步:选择规格
const specOk = await shop.selectSpec('黑色', '标准版');
console.info(`选择规格: ${specOk ? '✅' : '❌'}`);
// 第4步:加入购物车
const addOk = await shop.addToCart();
console.info(`加入购物车: ${addOk ? '✅' : '❌'}`);
if (!addOk) return;
// 第5步:进入购物车
const cartOk = await shop.goToCart();
console.info(`进入购物车: ${cartOk ? '✅' : '❌'}`);
if (!cartOk) return;
// 第6步:结算
const checkoutOk = await shop.checkout();
console.info(`结算: ${checkoutOk ? '✅' : '❌'}`);
if (!checkoutOk) return;
// 第7步:支付
const payOk = await shop.pay();
console.info(`支付: ${payOk ? '✅' : '❌'}`);
console.info('===== 购物流程测试完成 =====');
}
踩坑与注意事项
坑1:操作后立即验证
这是最最最常见的坑。click完了马上findComponent,大概率找不到。
// ❌ 点击后立即查找,UI还没更新
loginBtn.click();
const homeTitle = driver.findComponent(ON.text('首页')); // 大概率null
// ✅ 点击后等待一下
loginBtn.click();
await sleep(1000); // 等UI更新
const homeTitle = driver.findComponent(ON.text('首页'));
更好的做法是用assertComponentExistence带超时等待,而不是固定延时。
坑2:滑动的距离和方向
swipe的方向容易搞混。在UiTest框架中,SwipeDirection.UP表示手指向上滑——也就是内容向下滚,看到的是下方的内容。这和用户的直觉是一致的,但代码里写的时候容易搞反。
// SwipeDirection.UP = 手指向上滑 = 看到下方内容 = 列表向下滚
list.swipe(SwipeDirection.UP, 300); // 看下面的内容
list.swipe(SwipeDirection.DOWN, 300); // 看上面的内容
坑3:inputText的清空行为
inputText会先清空输入框的已有内容,再输入新文本。如果你想在已有文本后面追加内容,需要先获取当前文本,拼接后再输入。
// ❌ inputText会清空已有内容
const input = driver.findComponent(ON.id('search_input'));
input.inputText('Hello');
input.inputText(' World'); // 结果是" World",不是"Hello World"
// ✅ 追加内容需要先获取当前文本
const currentText = input.getText();
input.inputText(currentText + ' World');
坑4:长按的时间不够
longClick()默认长按时长是500ms,但有些组件的长按触发阈值是600ms甚至更长。如果你的长按操作没触发预期效果,试试增加长按时长。
坑5:拖拽的起点和终点
drag(target)是把当前组件拖到target组件的位置。但拖拽操作对目标区域的大小有要求——如果drop区域太小,拖拽可能无法正确识别为"放下"操作。
坑6:键盘操作的兼容性
pressKey模拟的是物理按键,但不是所有设备都有所有物理按键。比如有些平板没有BACK键,有些设备没有音量键。测试时要注意目标设备的按键配置。
HarmonyOS 6适配说明
HarmonyOS 6对操作模拟做了以下改进:
- 新增scrollTo操作:
component.scrollTo(ON.id('target'))直接滚动到目标组件可见,不需要手动计算滑动距离 - 手势录制回放:支持录制真实用户的手势序列,在测试中回放,适合复杂手势场景
- 操作速度可配置:click、swipe等操作可以设置执行速度,模拟不同用户的操作习惯
- 多点触控增强:支持同时模拟多个触摸点,适合测试多指操作场景
- 操作链(Action Chain):支持链式调用多个操作,中间可以插入延时,类似Appium的TouchAction
迁移注意:component.swipe(direction, speed)的speed参数单位从vp改为vp/s,旧代码如果传了speed参数需要调整数值。
总结
操作模拟是UI测试的核心能力。记住几个原则:操作后必须等待、滑动方向别搞反、输入注意清空行为、复杂操作要封装。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ 基础操作简单,手势组合需要理解坐标体系 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每个UI测试都在用 |
| 重要程度 | ⭐⭐⭐⭐⭐ 直接决定测试覆盖率 |
写操作模拟的代码,一定要把自己当成真实用户。用户点完按钮会等页面跳转,你也要等;用户滑列表会看内容加载完没有,你也要确认。别为了追求测试速度省掉等待时间——测试跑得快但结果不稳定,比跑得慢更可怕。
- 点赞
- 收藏
- 关注作者
评论(0)