HarmonyOS 6实战::多组件嵌套场景下,自动化测试覆盖复杂交互实践

HarmonyOS 6实战::多组件嵌套场景下,自动化测试覆盖复杂交互实践

先回顾一下我们之前的开发背景。

我们通过近10期的内容做了个AI旅行助手,用户可以在地图上规划路线、拖拽途经点、滑动查看详情面板。功能多,手势复杂------滑动地图、缩放、拖拽点、滑面板,好几个手势挤在一起。

之前我们专门写过一篇解决手势冲突的文章,当时调了好久才让它们和谐共处。

但问题没完。

每次改完代码,我都得手动点一遍所有功能------拖拽点会不会卡住?滑动面板还能不能吸到中档?地图缩放还跟手吗?

刚开始还能忍,后来功能越来越多,手动测试一遍要花十几分钟。更烦的是,有时候改了个看似无关的地方,某个角落的功能悄悄坏了,打补丁打累了。

于是决定引入UI自动化测试。后续计划也验证一下AI做自动化测试的可行性。

一、先看看我们要测什么

我们的AI助手页面UI有几项关键部分需要重点测试:

  • 底部有个可拖拽的面板,三档吸附(低、中、高)
  • 地图区域支持单指滑动、双指缩放
  • 路线上的点可以长按拖拽调整位置
  • 面板里有搜索框、功能按钮、足迹列表

需要测试的场景:

功能 测试点 之前的坑
拖拽面板 能否拖到高档、中档、低档 吸附不准,卡在半中间
地图滑动 单指滑动是否流畅 和拖拽点冲突
地图缩放 双指捏合是否响应 手势识别不稳定
拖拽路线点 长按后能否拖动 被地图滑动抢走
搜索输入 能否输入文字并搜索 键盘弹起后布局错乱
足迹列表 横向滚动是否正常 滚动和面板拖拽冲突

这些场景手动测一遍至少10分钟,而且容易漏。用自动化测试,跑一遍只要几秒钟。

二、UiTest引入与测试用例编写

鸿蒙提供了UiTest框架,放在@kit.TestKit里。核心就三个东西:

Driver :测试的"总指挥"。负责找控件、点按钮、滑动屏幕、按返回键。所有操作都是异步的,记得加await

ON :用来描述"我要找什么控件"。可以按文本找、按ID找、按类型找。支持链式调用,比如ON.text('确定').type('Button')

Component:找到控件后的操作对象。可以点它、双击它、长按它、往里面输文字。

一个典型的测试用例大概长这样:

typescript 复制代码
let driver = Driver.create();
let button = await driver.findComponent(ON.id('confirm_btn'));
await button.click();
await driver.assertComponentExist(ON.text('操作成功'));

结构很清楚:找控件 → 操作 → 断言结果。

测试代码放在entry/src/ohosTest/ets/test/目录下。我们按功能模块分了几个文件:

复制代码
entry/src/ohosTest/ets/test/
├── MapGesture.test.ets        # 地图手势测试
├── DragPanel.test.ets         # 拖拽面板测试
├── DragPoint.test.ets         # 拖拽路线点测试
├── SearchInput.test.ets       # 搜索输入测试
└── FootprintList.test.ets     # 足迹列表测试

每个文件对应一个功能模块,互不干扰。

测试用例写在describe块里,每个it是一个独立的测试用例。

typescript 复制代码
export default function mapGestureTest() {
  describe('地图手势测试', () => {
    beforeAll(async () => {
      // 启动应用,进入地图页面
    })
    
    it('单指滑动地图', async (done) => {
      // 测试代码
      done();
    })
    
    it('双指缩放地图', async (done) => {
      // 测试代码
      done();
    })
  })
}

三、几个核心场景的测试代码

为了节约上手成本,我采用了华为官方的AI助手辅助生成代码+手动调试的策略完成了下面的测试用例。

测试拖拽面板吸附

面板有三个档位:低档150vp、中档269vp、高档(接近全屏)。我们的测试要验证:拖拽到不同位置,松手后能否吸到正确的档位。

typescript 复制代码
// DragPanel.test.ets
it('拖拽面板到中档位置,松手后应吸附到中档', async (done) => {
  let driver = Driver.create();
  await driver.delayMs(2000); // 等待页面加载完成
  
  // 找到面板的拖拽指示条
  let dragBar = await driver.findComponent(ON.id('panel_drag_bar'));
  let bounds = await dragBar.getBounds();
  
  // 计算拖拽目标位置(向上拖拽到中档高度)
  let startX = bounds.centerX;
  let startY = bounds.centerY;
  let targetY = startY - 200; // 向上拖拽200像素
  
  // 执行拖拽
  await driver.swipe(startX, startY, startX, targetY);
  await driver.delayMs(500); // 等待吸附动画完成
  
  // 验证面板高度是否等于中档高度269
  let panel = await driver.findComponent(ON.id('bottom_panel'));
  let panelBounds = await panel.getBounds();
  let panelHeight = panelBounds.bottom - panelBounds.top;
  
  expect(Math.abs(panelHeight - 269) < 10).assertTrue(); // 允许±10像素误差
  
  done();
})

关键点

  • getBounds()获取组件位置,计算拖拽起点和终点
  • 拖拽后等待动画完成再断言
  • 断言允许少量误差,避免像素精度问题

测试拖拽路线点

路线点长按拖拽是最容易出问题的地方------之前一直被地图滑动手势抢走。

typescript 复制代码
// DragPoint.test.ets
it('长按路线点后拖拽,点应跟随手指移动', async (done) => {
  let driver = Driver.create();
  await driver.delayMs(2000);
  
  // 找到路线点(假设路线点有特定ID)
  let waypoint = await driver.findComponent(ON.id('waypoint_1'));
  let bounds = await waypoint.getBounds();
  
  // 长按路线点
  await waypoint.longClick();
  await driver.delayMs(300); // 等待长按触发
  
  // 拖拽到新位置
  let startX = bounds.centerX;
  let startY = bounds.centerY;
  let targetX = startX + 100;
  let targetY = startY - 50;
  
  await driver.swipe(startX, startY, targetX, targetY);
  await driver.delayMs(500);
  
  // 验证路线点位置已变化(通过检查坐标或地图上的标记)
  let newWaypoint = await driver.findComponent(ON.id('waypoint_1'));
  let newBounds = await newWaypoint.getBounds();
  
  // 新位置的x坐标应该增加了
  expect(newBounds.centerX > bounds.centerX).assertTrue();
  
  done();
})

关键点

  • longClick()模拟长按
  • 拖拽后用getBounds()验证位置变化
  • 注意长按和拖拽之间的延时,太短可能识别成普通点击

测试地图手势

地图手势测试比较特殊------地图本身没有明确的控件ID,我们需要用坐标来模拟操作。

typescript 复制代码
// MapGesture.test.ets
it('双指捏合应缩放地图', async (done) => {
  let driver = Driver.create();
  await driver.delayMs(2000);
  
  // 获取屏幕尺寸
  let display = await driver.getDisplayInfo();
  let screenWidth = display.width;
  let screenHeight = display.height;
  
  // 双指捏合:两个手指从中心向中间收拢
  // 手指1:从(centerX-100, centerY) 移动到 (centerX-50, centerY)
  // 手指2:从(centerX+100, centerY) 移动到 (centerX+50, centerY)
  
  // 注意:鸿蒙的swipe是单指,双指捏合需要用pinch方法
  // 这里用坐标点模拟
  let centerX = screenWidth / 2;
  let centerY = screenHeight / 2;
  
  // 先记录缩放前的可见区域(通过地图边界判断)
  // 这里简化处理,实际可能需要读取地图组件的zoomLevel
  
  // 执行捏合操作(具体API参考文档)
  await driver.pinch(centerX, centerY, 200, 'in'); // 向内捏合
  
  await driver.delayMs(1000);
  
  // 验证缩放效果(通过检查地图上某个固定点的位置变化)
  // 这里假设地图中心点变了,或者zoomLevel变了
  
  done();
})

关键点

  • 地图没有ID,需要用坐标定位
  • 双指操作需要用专门的pinch方法
  • 验证缩放需要读取地图状态或对比截图

测试搜索输入

搜索框的测试主要验证输入和清空功能。

typescript 复制代码
// SearchInput.test.ets
it('在搜索框输入文字,应能正确显示', async (done) => {
  let driver = Driver.create();
  await driver.delayMs(2000);
  
  // 找到搜索框
  let searchInput = await driver.findComponent(ON.id('search_input'));
  
  // 输入文字
  await searchInput.inputText('故宫');
  await driver.delayMs(500);
  
  // 验证输入结果
  let inputText = await searchInput.getText();
  expect(inputText).assertEqual('故宫');
  
  // 测试清空
  await searchInput.clearText();
  await driver.delayMs(300);
  
  let clearedText = await searchInput.getText();
  expect(clearedText).assertEqual('');
  
  done();
})

关键点

  • inputText()支持中英文
  • getText()获取输入框当前内容
  • clearText()一键清空

测试足迹列表横向滚动

足迹列表是一个横向滚动的Scroll,需要验证左右滑动能切换显示的内容。

typescript 复制代码
// FootprintList.test.ets
it('横向滑动足迹列表,应切换显示不同城市卡片', async (done) => {
  let driver = Driver.create();
  await driver.delayMs(2000);
  
  // 找到足迹列表容器
  let listContainer = await driver.findComponent(ON.id('footprint_list'));
  let bounds = await listContainer.getBounds();
  
  // 记录滑动前的第一个城市
  let firstCard = await driver.findComponent(ON.id('city_card_0'));
  let firstName = await firstCard.getText();
  
  // 横向向左滑动
  let startX = bounds.right - 50;
  let endX = bounds.left + 50;
  let y = bounds.centerY;
  
  await driver.swipe(startX, y, endX, y);
  await driver.delayMs(500);
  
  // 滑动后,原来的第一个卡片应该不可见
  let oldCardExists = await driver.findComponent(ON.id('city_card_0')).catch(() => null);
  expect(oldCardExists).assertNull();
  
  // 新的卡片应该出现
  let newCard = await driver.findComponent(ON.id('city_card_1'));
  expect(newCard).assertNotNull();
  
  done();
})

踩过的坑

坑1:控件生命周期相关问题

页面打开后,控件可能还没渲染完。直接findComponent会返回null。

解决方案:加delayMswaitForIdle

typescript 复制代码
await driver.waitForIdle(4000, 5000); // 等待UI稳定

拖拽面板有吸附动画,动画没结束时断言会拿到错误的高度。

解决方案:拖拽后加延时等待动画完成。

typescript 复制代码
await driver.swipe(...);
await driver.delayMs(500); // 等待动画

坑2:控件ID写错或者忘记加

测试代码里用ON.id('xxx')找控件,但实际页面上可能忘了给控件加.id('xxx')

解决方案:写代码的时候就顺手给关键控件加上ID。

typescript 复制代码
// 页面代码
Button('提交')
  .id('submit_btn')  // 加ID,测试好用
  
// 测试代码
let btn = await driver.findComponent(ON.id('submit_btn'));

坑3:坐标计算依赖屏幕尺寸

不同设备屏幕尺寸不一样,写死坐标会在别的设备上跑不通。

解决方案:用getBounds()动态获取组件位置,或者用相对坐标。

typescript 复制代码
let bounds = await component.getBounds();
let centerX = bounds.centerX;
let centerY = bounds.centerY;

坑4:手势冲突场景测不准

地图手势和面板手势同时存在时,测试代码模拟的拖拽可能被系统识别成别的操作。

解决方案:测试时把无关手势临时禁用,或者用更精确的坐标控制。

总结

鸿蒙的UiTest框架用起来不算复杂,核心就是三件事:

步骤 怎么做
找控件 driver.findComponent(ON.id('xxx'))
操作控件 component.click() / component.inputText() / driver.swipe()
验证结果 expect(actual).assertEqual(expected)

我们的经验是:核心功能一定要有测试覆盖。比如拖拽面板吸附、路线点拖拽、地图手势这些容易出问题的场景,写测试用例花的半小时,可能帮你省下后面好几个小时的调试时间。

回归测试不再靠人肉。每次改完代码,跑一遍测试用例,几分钟就知道有没有破坏现有功能。

问题发现更早。之前往往是用户反馈了才知道有bug,现在测试用例跑不过就能发现。

重构敢下手了。之前有些代码写得烂但不敢动,怕改坏了。现在有测试兜底,敢改了。

当然,写测试也有成本。一个测试用例少则几十行,多则上百行。但长远来看,在AI辅助的情况下,省下的调试时间远超写测试的时间。

相关资源TestKit API参考

相关推荐
以太浮标3 小时前
华为eNSP模拟器综合实验之- IS-IS路由协议实践配置解析
网络协议·网络安全·华为·智能路由器·信息与通信
嵌入式×边缘AI:打怪升级日志4 小时前
Linux 驱动开发入门:从最简单的 hello 驱动到硬件交互
linux·驱动开发·交互
键盘鼓手苏苏6 小时前
Flutter 三方库 pip 的鸿蒙化适配指南 - 实现标准化的画中画(Picture-in-Picture)模式、支持视频悬浮窗与多任务并行交互
flutter·pip·harmonyos
左手厨刀右手茼蒿6 小时前
Flutter 组件 sheety_localization 的适配 鸿蒙Harmony 实战 - 驾驭在线协作式多语言管理、实现鸿蒙端动态词条下发与全球化敏捷发布方案
flutter·harmonyos·鸿蒙·openharmony·sheety_localization
见山是山-见水是水7 小时前
鸿蒙flutter第三方库适配 - 路由书签应用
flutter·华为·harmonyos
火柴就是我7 小时前
记录一些跨平台开发需要的鸿蒙知识
flutter·harmonyos
小雨青年8 小时前
鸿蒙 HarmonyOS 6 | 空间音频技术实战指南
华为·音视频·harmonyos
Huanzhi_Lin9 小时前
鸿蒙NEXT-HelloWorld
华为·harmonyos·arkts·arkui·ets
特立独行的猫a10 小时前
使用 vcpkg 为鸿蒙(HarmonyOS / OHOS)下载与安装三方库实践指南
华为·harmonyos·openharmony·vcpkg·三方库·鸿蒙pc