HarmonyOS开发:兼容性测试——多设备测试
HarmonyOS开发:兼容性测试——多设备测试
📌 核心要点:兼容性测试确保App在不同屏幕尺寸、分辨率、折叠形态下UI表现一致,核心是响应式布局验证和折叠屏适配,自动化测试覆盖关键场景,手动测试兜底边缘情况。
背景与动机
你的App在手机上完美,在平板上呢?列表项被截断了,按钮挤到一起了,弹窗只显示了一半。
HarmonyOS的设备生态太丰富了——手机、平板、折叠屏、智慧屏、车机,屏幕尺寸从4英寸到86英寸都有。同一个App要跑在这么多设备上,UI适配工作量巨大。
更麻烦的是折叠屏。展开和折叠两种形态,屏幕尺寸瞬间变化,你的布局能跟着变吗?折叠时正在看的页面,展开后还在吗?分屏模式下两个App同时显示,你的UI会乱吗?
手动在每台设备上测一遍?设备买全了都要好几万,更别说测试时间了。所以你需要自动化兼容性测试——用脚本在不同设备(或不同模拟器配置)上跑同一套测试,自动发现适配问题。
核心原理
兼容性测试的维度
graph TB
A[兼容性测试] --> B[屏幕尺寸]
A --> C[分辨率]
A --> D[折叠形态]
A --> E[横竖屏]
A --> F[特殊比例]
B --> B1["手机: 6-7英寸"]
B --> B2["平板: 10-12英寸"]
B --> B3["折叠屏: 6.5/8英寸"]
C --> C1["FHD: 1080×2340"]
C --> C2["2K: 1440×3200"]
C --> C3["平板: 1600×2560"]
D --> D1["折叠态: 窄屏"]
D --> D2["展开态: 宽屏"]
D --> D3["折叠中: 动态切换"]
E --> E1["竖屏: 默认"]
E --> E2["横屏: 视频场景"]
E --> E3["自由旋转"]
F --> F1["长屏: 折叠屏外屏"]
F --> F2["方屏: 智慧屏"]
classDef root fill:#4A90D9,stroke:#2C5F8A,color:#fff
classDef size fill:#67C23A,stroke:#3E9B2B,color:#fff
classDef res fill:#E6A23C,stroke:#B07D2B,color:#fff
classDef fold fill:#F56C6C,stroke:#C94A4A,color:#fff
classDef orient fill:#9B59B6,stroke:#7D3C98,color:#fff
classDef special fill:#909399,stroke:#606266,color:#fff
class A root
class B,B1,B2,B3 size
class C,C1,C2,C3 res
class D,D1,D2,D3 fold
class E,E1,E2,E3 orient
class F,F1,F2 special
折叠屏适配测试流程
折叠屏是兼容性测试中最复杂的场景,因为涉及运行时的屏幕尺寸变化:
flowchart TD
A[折叠态运行App] --> B[验证窄屏布局]
B --> C[展开折叠屏]
C --> D{App响应折叠事件?}
D -->|是| E[验证宽屏布局]
D -->|否| F[App可能重启]
F --> G[验证重启后状态恢复]
E --> H[折叠回去]
G --> H
H --> I[验证恢复窄屏布局]
I --> J[验证数据/状态未丢失]
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 check fill:#9B59B6,stroke:#7D3C98,color:#fff
class A start
class B,E,H,I,J process
class C,F process
class D decision
class G check
代码实战
基础用法:屏幕尺寸验证
// 467_compatibility_test_basic.ets
import { Driver, ON } from '@ohos.UiTest';
import { display } from '@kit.ArkUI';
export default function basicCompatibilityTest() {
const driver = Driver.create();
// ===== 1. 获取当前设备屏幕信息 =====
const displayInfo = display.getDefaultDisplaySync();
console.info(`屏幕尺寸: ${displayInfo.width}×${displayInfo.height}`);
console.info(`屏幕密度: ${displayInfo.densityDPI} dpi`);
console.info(`刷新率: ${displayInfo.refreshRate} Hz`);
// ===== 2. 验证关键组件在不同尺寸下的可见性 =====
// 不管屏幕多大,核心功能组件都应该可见
// 搜索框应该可见
const searchBox = driver.findComponent(ON.id('search_box'));
if (searchBox === null) {
console.error(`❌ [${displayInfo.width}×${displayInfo.height}] 搜索框不可见`);
} else {
console.info(`✅ [${displayInfo.width}×${displayInfo.height}] 搜索框可见`);
}
// 底部导航栏应该可见
const tabBar = driver.findComponent(ON.id('tab_bar'));
if (tabBar === null) {
console.error(`❌ [${displayInfo.width}×${displayInfo.height}] 底部导航不可见`);
} else {
// 验证Tab数量
const tabs = tabBar.findComponents(ON.type('TabContent'));
console.info(`✅ [${displayInfo.width}×${displayInfo.height}] 底部导航有${tabs.length}个Tab`);
}
// ===== 3. 验证组件不超出屏幕边界 =====
const submitBtn = driver.findComponent(ON.id('submit_btn'));
if (submitBtn !== null) {
const bounds = submitBtn.getBounds();
const isWithinScreen =
bounds.left >= 0 &&
bounds.top >= 0 &&
bounds.right <= displayInfo.width &&
bounds.bottom <= displayInfo.height;
if (!isWithinScreen) {
console.error(`❌ 提交按钮超出屏幕边界: ${JSON.stringify(bounds)}`);
} else {
console.info('✅ 提交按钮在屏幕范围内');
}
}
// ===== 4. 验证文字不截断 =====
const titleText = driver.findComponent(ON.id('product_title'));
if (titleText !== null) {
const textBounds = titleText.getBounds();
const parentBounds = titleText.getParent()?.getBounds();
if (parentBounds !== null) {
// 检查文字是否被父容器截断
const isTruncated = textBounds.width > parentBounds.width;
if (isTruncated) {
console.error('❌ 商品标题被截断');
} else {
console.info('✅ 商品标题完整显示');
}
}
}
}
进阶用法:折叠屏与横竖屏测试
// 467_compatibility_test_advanced.ets
import { Driver, ON } from '@ohos.UiTest';
import { display } from '@kit.ArkUI';
// 折叠屏测试工具
class FoldableTestHelper {
private driver: Driver;
constructor(driver: Driver) {
this.driver = driver;
}
// 获取当前折叠状态
getFoldStatus(): string {
const displayInfo = display.getDefaultDisplaySync();
// 根据屏幕宽度判断折叠状态
// 折叠态通常宽度 < 400vp,展开态 > 400vp
if (displayInfo.width < 400) {
return 'FOLDED';
} else {
return 'UNFOLDED';
}
}
// 测试折叠→展开的布局适配
async testFoldToUnfold(): Promise<FoldTestResult> {
const result: FoldTestResult = {
initialLayout: '',
finalLayout: '',
statePreserved: false,
layoutAdapted: false,
errors: [],
};
// 第1步:记录折叠态的UI状态
result.initialLayout = this.getFoldStatus();
const foldedTitle = this.getComponentText(ON.id('page_title'));
const foldedScrollPos = this.getScrollPosition(ON.id('main_list'));
// 第2步:模拟展开(通过改变窗口尺寸)
// 在测试环境中,可以通过设置窗口尺寸模拟折叠屏展开
await this.simulateUnfold();
// 第3步:验证展开后的布局
result.finalLayout = this.getFoldStatus();
// 验证页面标题还在
const unfoldedTitle = this.getComponentText(ON.id('page_title'));
if (unfoldedTitle !== foldedTitle) {
result.errors.push(`页面标题变化: "${foldedTitle}" → "${unfoldedTitle}"`);
}
// 验证滚动位置是否保持
const unfoldedScrollPos = this.getScrollPosition(ON.id('main_list'));
result.statePreserved = Math.abs(unfoldedScrollPos - foldedScrollPos) < 50;
// 验证布局是否适配宽屏
result.layoutAdapted = this.verifyWideScreenLayout();
return result;
}
// 测试展开→折叠的布局适配
async testUnfoldToFold(): Promise<FoldTestResult> {
const result: FoldTestResult = {
initialLayout: '',
finalLayout: '',
statePreserved: false,
layoutAdapted: false,
errors: [],
};
// 先确保在展开态
await this.simulateUnfold();
result.initialLayout = 'UNFOLDED';
const unfoldedTitle = this.getComponentText(ON.id('page_title'));
// 折叠
await this.simulateFold();
result.finalLayout = 'FOLDED';
// 验证标题
const foldedTitle = this.getComponentText(ON.id('page_title'));
if (foldedTitle !== unfoldedTitle) {
result.errors.push(`页面标题变化: "${unfoldedTitle}" → "${foldedTitle}"`);
}
// 验证窄屏布局
result.layoutAdapted = this.verifyNarrowScreenLayout();
return result;
}
// 验证宽屏布局
private verifyWideScreenLayout(): boolean {
const displayInfo = display.getDefaultDisplaySync();
let passed = true;
// 宽屏下应该显示侧边栏或双列布局
const sideNav = this.driver.findComponent(ON.id('side_navigation'));
if (sideNav === null) {
// 没有侧边栏也行,但列表应该是多列
const list = this.driver.findComponent(ON.id('product_list'));
if (list !== null) {
const listBounds = list.getBounds();
// 宽屏下列表宽度应该超过屏幕60%
if (listBounds.width < displayInfo.width * 0.6) {
passed = false;
}
}
}
return passed;
}
// 验证窄屏布局
private verifyNarrowScreenLayout(): boolean {
let passed = true;
// 窄屏下不应该有侧边栏
const sideNav = this.driver.findComponent(ON.id('side_navigation'));
if (sideNav !== null) {
const navBounds = sideNav.getBounds();
// 侧边栏在窄屏下应该隐藏或折叠
if (navBounds.width > 0) {
passed = false;
}
}
// 底部导航应该可见
const bottomNav = this.driver.findComponent(ON.id('tab_bar'));
if (bottomNav === null) {
passed = false;
}
return passed;
}
// 模拟展开
private async simulateUnfold(): Promise<void> {
// 在测试环境中通过API模拟折叠屏展开
// 实际实现取决于测试框架的设备模拟能力
await this.sleep(1000); // 等待布局重排
}
// 模拟折叠
private async simulateFold(): Promise<void> {
await this.sleep(1000);
}
// 辅助方法
private getComponentText(selector: ON): string {
const component = this.driver.findComponent(selector);
return component?.getText() ?? '';
}
private getScrollPosition(selector: ON): number {
const component = this.driver.findComponent(selector);
if (component === null) return 0;
const bounds = component.getBounds();
return bounds.top;
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
interface FoldTestResult {
initialLayout: string;
finalLayout: string;
statePreserved: boolean;
layoutAdapted: boolean;
errors: string[];
}
完整示例:多设备兼容性测试套件
// 467_compatibility_test_full.ets
import { Driver, ON } from '@ohos.UiTest';
import { display } from '@kit.ArkUI';
// 设备配置
interface DeviceProfile {
name: string;
width: number;
height: number;
density: number;
type: 'phone' | 'tablet' | 'foldable';
}
const DEVICE_PROFILES: DeviceProfile[] = [
{ name: '标准手机', width: 1080, height: 2340, density: 480, type: 'phone' },
{ name: '大屏手机', width: 1440, height: 3200, density: 560, type: 'phone' },
{ name: '折叠屏(折叠)', width: 1080, height: 2340, density: 480, type: 'foldable' },
{ name: '折叠屏(展开)', width: 2208, height: 1840, density: 480, type: 'foldable' },
{ name: '平板', width: 1600, height: 2560, density: 320, type: 'tablet' },
];
// 兼容性测试套件
class CompatibilityTestSuite {
private driver: Driver;
private results: Map<string, CompatibilityResult>;
constructor() {
this.driver = Driver.create();
this.results = new Map();
}
// 在当前设备上运行测试
async testOnCurrentDevice(): Promise<void> {
const displayInfo = display.getDefaultDisplaySync();
const deviceName = this.identifyDevice(displayInfo);
console.info(`\n📱 在 ${deviceName} (${displayInfo.width}×${displayInfo.height}) 上运行兼容性测试`);
const result: CompatibilityResult = {
deviceName,
width: displayInfo.width,
height: displayInfo.height,
checks: [],
};
// 检查1:关键组件可见性
result.checks.push(this.checkComponentVisibility('搜索框', ON.id('search_box')));
result.checks.push(this.checkComponentVisibility('底部导航', ON.id('tab_bar')));
result.checks.push(this.checkComponentVisibility('内容区域', ON.id('main_content')));
// 检查2:组件不超出屏幕
result.checks.push(this.checkComponentWithinScreen('提交按钮', ON.id('submit_btn'), displayInfo));
result.checks.push(this.checkComponentWithinScreen('底部导航', ON.id('tab_bar'), displayInfo));
// 检查3:文字不截断
result.checks.push(this.checkTextNotTruncated('商品标题', ON.id('product_title')));
result.checks.push(this.checkTextNotTruncated('价格标签', ON.id('price_label')));
// 检查4:间距合理
result.checks.push(this.checkReasonableSpacing('列表项间距', ON.id('product_list')));
// 检查5:触摸目标大小
result.checks.push(this.checkTouchTargetSize('按钮', ON.id('submit_btn'), 44));
result.checks.push(this.checkTouchTargetSize('Tab', ON.id('tab_home'), 44));
// 根据设备类型做额外检查
if (displayInfo.width > 1400) {
// 宽屏设备检查
result.checks.push(this.checkWideScreenLayout());
} else {
// 窄屏设备检查
result.checks.push(this.checkNarrowScreenLayout());
}
this.results.set(deviceName, result);
}
// 检查组件可见性
private checkComponentVisibility(name: string, selector: ON): CheckResult {
const component = this.driver.findComponent(selector);
const visible = component !== null;
return {
name: `可见性: ${name}`,
passed: visible,
detail: visible ? '组件可见' : '组件不可见',
};
}
// 检查组件在屏幕内
private checkComponentWithinScreen(name: string, selector: ON, displayInfo: display.Display): CheckResult {
const component = this.driver.findComponent(selector);
if (component === null) {
return { name: `屏幕内: ${name}`, passed: false, detail: '组件不存在' };
}
const bounds = component.getBounds();
const within = bounds.left >= 0 && bounds.top >= 0 &&
bounds.right <= displayInfo.width &&
bounds.bottom <= displayInfo.height;
return {
name: `屏幕内: ${name}`,
passed: within,
detail: within ? '在屏幕内' : `超出屏幕: ${JSON.stringify(bounds)}`,
};
}
// 检查文字不截断
private checkTextNotTruncated(name: string, selector: ON): CheckResult {
const component = this.driver.findComponent(selector);
if (component === null) {
return { name: `截断: ${name}`, passed: true, detail: '组件不存在,跳过' };
}
// 简化判断:检查组件宽度是否合理
const bounds = component.getBounds();
const text = component.getText();
const estimatedWidth = text.length * 14; // 估算文字宽度
const isTruncated = bounds.width < estimatedWidth * 0.5;
return {
name: `截断: ${name}`,
passed: !isTruncated,
detail: isTruncated ? '文字可能被截断' : '文字完整显示',
};
}
// 检查间距合理
private checkReasonableSpacing(name: string, selector: ON): CheckResult {
const component = this.driver.findComponent(selector);
if (component === null) {
return { name: `间距: ${name}`, passed: true, detail: '组件不存在,跳过' };
}
const items = component.findComponents(ON.type('ListItem'));
if (items.length < 2) {
return { name: `间距: ${name}`, passed: true, detail: '列表项不足,跳过' };
}
// 检查相邻项之间的间距
const bounds1 = items[0].getBounds();
const bounds2 = items[1].getBounds();
const gap = bounds2.top - bounds1.bottom;
const reasonable = gap >= 4 && gap <= 32; // 间距在4-32vp之间
return {
name: `间距: ${name}`,
passed: reasonable,
detail: `间距: ${gap}vp ${reasonable ? '合理' : '异常'}`,
};
}
// 检查触摸目标大小
private checkTouchTargetSize(name: string, selector: ON, minSize: number): CheckResult {
const component = this.driver.findComponent(selector);
if (component === null) {
return { name: `触摸: ${name}`, passed: false, detail: '组件不存在' };
}
const bounds = component.getBounds();
const meetsMinWidth = bounds.width >= minSize;
const meetsMinHeight = bounds.height >= minSize;
return {
name: `触摸: ${name}`,
passed: meetsMinWidth && meetsMinHeight,
detail: `尺寸: ${bounds.width}×${bounds.height}vp (最小${minSize}vp)`,
};
}
// 宽屏布局检查
private checkWideScreenLayout(): CheckResult {
const sideNav = this.driver.findComponent(ON.id('side_navigation'));
const hasSideNav = sideNav !== null;
return {
name: '宽屏布局',
passed: true, // 宽屏布局有多种合理方案
detail: hasSideNav ? '使用侧边导航布局' : '使用全宽布局',
};
}
// 窄屏布局检查
private checkNarrowScreenLayout(): CheckResult {
const bottomNav = this.driver.findComponent(ON.id('tab_bar'));
const hasBottomNav = bottomNav !== null;
return {
name: '窄屏布局',
passed: hasBottomNav,
detail: hasBottomNav ? '使用底部导航布局' : '缺少底部导航',
};
}
// 识别设备
private identifyDevice(displayInfo: display.Display): string {
const width = displayInfo.width;
if (width < 1200) return '手机';
if (width < 1800) return '折叠屏展开';
return '平板';
}
// 打印报告
printReport(): void {
console.info('\n📱 ===== 兼容性测试报告 =====');
for (const [device, result] of this.results) {
console.info(`\n📱 ${device} (${result.width}×${result.height})`);
console.info('─'.repeat(50));
for (const check of result.checks) {
const status = check.passed ? '✅' : '❌';
console.info(` ${status} ${check.name}: ${check.detail}`);
}
const passed = result.checks.filter(c => c.passed).length;
const total = result.checks.length;
console.info(` 通过: ${passed}/${total}`);
}
}
}
interface CompatibilityResult {
deviceName: string;
width: number;
height: number;
checks: CheckResult[];
}
interface CheckResult {
name: string;
passed: boolean;
detail: string;
}
// 执行测试
export default async function runCompatibilityTest() {
const suite = new CompatibilityTestSuite();
await suite.testOnCurrentDevice();
suite.printReport();
}
踩坑与注意事项
坑1:模拟器和真机的渲染差异
模拟器上看着没问题的布局,真机上可能有问题。特别是字体渲染、圆角抗锯齿、阴影效果这些,模拟器和真机差异很大。
建议:关键兼容性测试一定要在真机上验证,模拟器只做初步筛查。
坑2:折叠屏的状态保持
折叠屏展开/折叠时,App可能被系统重启。如果你的测试假设App一直在运行,就会遇到"组件找不到"的问题——因为App重启了,之前的页面状态全丢了。
解决方案:测试折叠切换时,先检查App是否还在前台,如果被重启了需要重新导航到目标页面。
坑3:横竖屏切换的数据丢失
有些App在横竖屏切换时会重新创建Activity/Ability,导致页面数据丢失。测试时要验证切换后数据是否还在。
坑4:不同密度下的尺寸换算
vp和px的换算关系在不同密度设备上不同。1vp在480dpi设备上是3px,在320dpi设备上是2px。测试时要注意用哪个单位做判断。
// vp转px
const px = vp * displayInfo.densityPixels;
// px转vp
const vp = px / displayInfo.densityPixels;
坑5:安全区域适配
全面屏设备有刘海、圆角、底部手势条等安全区域。你的UI内容不能被这些区域遮挡。兼容性测试要验证关键内容不在安全区域外。
HarmonyOS 6适配说明
HarmonyOS 6对兼容性测试做了以下增强:
- 虚拟设备管理:测试框架内置虚拟设备管理,可以一键切换不同设备配置,不需要物理设备
- 折叠屏模拟增强:支持在模拟器上实时模拟折叠/展开操作,测试布局动态响应
- 多窗口测试:支持分屏、自由窗口等场景的自动化测试
- 安全区域检测:内置安全区域检测API,自动验证UI是否被遮挡
- 响应式布局验证:新增布局断点检测,验证不同断点下的布局是否正确
迁移注意:display.getDefaultDisplaySync()在API 13中被标记为deprecated,建议使用display.getAllDisplays()获取显示信息。
总结
兼容性测试的核心是"覆盖关键设备,验证关键场景"。你不需要在所有设备上测所有功能,但必须在主流设备上验证核心流程。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ 基础验证简单,折叠屏适配需要深入理解 |
| 使用频率 | ⭐⭐⭐⭐ 多设备生态下必做 |
| 重要程度 | ⭐⭐⭐⭐⭐ 直接影响用户覆盖面 |
记住:兼容性测试不是"在所有设备上跑一遍",而是"确保核心体验在所有设备上都达标"。优先保证手机和平板的核心流程,再逐步覆盖折叠屏、智慧屏等特殊设备。
- 点赞
- 收藏
- 关注作者
评论(0)